project-root/ ├── src/ # 產品代碼 │ ├── Models/ │ │ └── User.php │ ├── Services/ │ │ └── AuthService.php │ └── Utils/ │ └── Validator.php ├── tests/ # 測試代碼 (鏡像 src 結構) │ ├── bootstrap.php # 自動載入、常數、測試專用配置 │ ├── Models/ │ │ └── UserTest.php │ ├── Services/ │ │ └── AuthServiceTest.php │ └── Utils/ │ └── ValidatorTest.php ├── vendor/ # Composer 依賴 (含 simpletest/simpletest) ├── phpunit.xml.dist # (若併用 PHPUnit) └── .github/workflows/ci.yml
<?php
// tests/bootstrap.php
define('APP_ROOT', __DIR__ . '/..');
define('TEST_MODE', true);
// Composer 自動載入
require_once APP_ROOT . '/vendor/autoload.php';
// 簡易自動載入 (若專案未用 PSR-4)
spl_autoload_register(function ($class) {
$file = APP_ROOT . '/src/' . str_replace('\\', '/', $class) . '.php';
if (is_file($file)) require_once $file;
});
// 測試專用配置 (如 SQLite 內存資料庫)
putenv('DB_DSN=sqlite::memory:');
putenv('CACHE_DRIVER=array');
// ...
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once 'simpletest/autorun.php'; // 自動執行
require_once 'simpletest/web_tester.php'; // 若需 Web 測試
class UserTest extends UnitTestCase {
private $user;
function setUp() {
$this->user = new User([
'email' => 'test@example.com',
'password' => 'secret123',
'name' => 'Test User',
]);
}
function tearDown() {
unset($this->user);
}
// 基本屬性測試
function testUserCreation() {
$this->assertEqual($this->user->email, 'test@example.com');
$this->assertTrue($this->user->validateEmail());
$this->assertFalse($this->user->isAdmin());
}
// 密碼雜湊驗證
function testPasswordHashing() {
$this->assertTrue($this->user->checkPassword('secret123'));
$this->assertFalse($this->user->checkPassword('wrong'));
$this->assertNotEqual($this->user->password, 'secret123'); // 應為雜湊
}
// 例外情況
function testInvalidEmailThrows() {
$this->expectException('InvalidArgumentException');
new User(['email' => 'invalid', 'password' => 'x', 'name' => 'X']);
}
// Mock 依賴測試 (如發送歡迎郵件)
function testRegisterSendsWelcomeEmail() {
$mailer = $this->mock('MailerInterface');
$mailer->expectOnce('send', ['welcome', 'test@example.com']);
$service = new AuthService($mailer);
$service->register($this->user);
}
}
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once 'simpletest/autorun.php';
require_once 'simpletest/web_tester.php';
class AuthServiceWebTest extends WebTestCase {
function testLoginPageLoads() {
$this->get('https://localhost/test/login.php');
$this->assertResponse(200);
$this->assertTitle('登入');
$this->assertFieldByName('email');
$this->assertFieldByName('password');
}
function testSuccessfulLogin() {
$this->get('https://localhost/test/login.php');
$this->setField('email', 'test@example.com');
$this->setField('password', 'secret123');
$this->clickSubmit('登入');
$this->assertResponse(302); // 重導向
$this->assertCookie('session_id');
$this->followRedirect();
$this->assertText('歡迎回來');
}
}
| 層級 | 目標 | 工具/方法 | 執行頻率 |
|---|---|---|---|
| 單元測試 | 純邏輯函數/類別,無外部依賴 | SimpleTest `UnitTestCase` + Mock | 每次 commit (pre-commit hook / CI) |
| 整合測試 | 模組間互動、資料庫、快取、外部 API | SimpleTest `WebTestCase` / `DatabaseTestCase` | 每次 push / PR (CI 階段) |
| 端到端測試 | 真實瀏覽器流程 (登入、下單、付款) | Codeception / Cypress / Playwright | 每日 / 發布前 |
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8
env: { MYSQL_ROOT_PASSWORD: secret, MYSQL_DATABASE: test }
ports: [3306:3306]
options: --health-cmd="mysqladmin ping" --health-interval=10s
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with: { php-version: '8.2', extensions: mbstring, pdo_mysql, sqlite3, coverage: xdebug }
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run unit tests
run: php vendor/bin/simpletest tests/ --tap | tee test_results.tap
- name: Upload coverage
if: always()
uses: actions/upload-artifact@v4
with: { name: coverage, path: build/coverage }
SimpleTest 社群活躍度較低,新專案建議直接採用 **PHPUnit 10+**;遷移步驟: