凌晨 3 點,你是否擔心剛上線的代碼會崩潰?單元測試通過自動化測試用例,在重構、重大變更、部署前後快速驗證代碼行為,建立信心,讓你安心入睡。測試驅動開發 (TDD) 更主張「先寫測試、再寫實現」,確保每行代碼都有測試覆蓋。
# 安裝 PHPUnit(現代項目建議用 Composer)
composer require --dev phpunit/phpunit ^9.5
# 或舊版 PEAR 安裝(本文範例基於 PHPUnit 2.x 風格)
pear install PHPUnit2
<?php
// TestAdd.php
require_once 'Add.php';
require_once 'PHPUnit2/Framework/TestCase.php';
class TestAdd extends PHPUnit2_Framework_TestCase {
public function testAddPositive() {
$this->assertEquals(3, add(1, 2));
}
public function testAddSame() {
$this->assertEquals(2, add(1, 1));
}
}
?>
<?php
// Add.php - 故意寫錯
function add($a, $b) { return 0; }
?>
$ phpunit TestAdd.php
# 顯示 2 failures - 測試有效,能捕獲錯誤
<?php
// Add.php - 正確實現
function add($a, $b) { return $a + $b; }
?>
$ phpunit TestAdd.php
OK (2 tests) - 所有測試通過
核心挑戰:每次測試前將數據庫重置到已知狀態,且不可污染生產數據。
-- schema.sql
DROP TABLE IF EXISTS authors;
CREATE TABLE authors (
id MEDIUMINT NOT NULL AUTO_INCREMENT,
name TEXT NOT NULL,
PRIMARY KEY (id)
);
<?php
// TestAuthors.php
require_once 'dblib.php';
require_once 'PHPUnit2/Framework/TestCase.php';
class TestAuthors extends PHPUnit2_Framework_TestCase {
public function testDeleteAll() {
$this->assertTrue(Authors::delete_all());
}
public function testInsert() {
$this->assertTrue(Authors::delete_all());
$this->assertTrue(Authors::insert('Jack'));
}
public function testInsertAndGet() {
$this->assertTrue(Authors::delete_all());
$this->assertTrue(Authors::insert('Jack'));
$this->assertTrue(Authors::insert('Joe'));
$found = Authors::get_all();
$this->assertNotNull($found);
$this->assertCount(2, $found);
}
}
?>
<?php
// dblib.php - 最終版
require_once 'DB.php';
class Authors {
public static function get_db() {
$dsn = 'mysql://root:password@localhost/unitdb';
$db =& DB::connect($dsn, array());
if (PEAR::isError($db)) die($db->getMessage());
return $db;
}
public static function delete_all() {
$db = self::get_db();
$sth = $db->prepare('DELETE FROM authors');
$db->execute($sth);
return true;
}
public static function insert($name) {
$db = self::get_db();
$sth = $db->prepare('INSERT INTO authors VALUES (null, ?)');
$db->execute($sth, array($name));
return true;
}
public static function get_all() {
$db = self::get_db();
$res = $db->query('SELECT * FROM authors');
$rows = array();
while ($res->fetchInto($row)) $rows[] = $row;
return $rows;
}
}
?>
<?php
// TestPage.php
require_once 'HTTP/Client.php';
require_once 'PHPUnit2/Framework/TestCase.php';
class TestPage extends PHPUnit2_Framework_TestCase {
private $client;
protected function setUp() {
$this->client = new HTTP_Client();
}
public function testHomePage() {
$this->client->get('https://localhost/');
$this->assertEquals(200, $this->client->currentResponse()->status);
$this->assertStringContainsString('Welcome', $this->client->currentResponse()->body);
}
}
?>
test[Method]_[Scenario]_[Expected] 命名單元測試不是負擔,而是重構的安全網、文檔的活樣本、設計的審查員。從今天為每個新功能寫第一個測試開始,漸進式提升覆蓋率,你的代碼庫將變得更健壯、更自信。