本文目录导读:

我来详细讲解如何为PHP项目编写单元测试。
选择测试框架
主流选择:PHPUnit
# 使用Composer安装PHPUnit composer require --dev phpunit/phpunit # 或全局安装 wget https://phar.phpunit.de/phpunit.phar
基本项目结构
project/
├── src/
│ ├── Calculator.php
│ └── User.php
├── tests/
│ ├── Unit/
│ │ ├── CalculatorTest.php
│ │ └── UserTest.php
│ └── bootstrap.php
├── composer.json
└── phpunit.xml
配置PHPUnit
phpunit.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
verbose="true"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit Tests">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>
编写第一个测试
待测试的类
<?php
// src/Calculator.php
class Calculator {
public function add($a, $b) {
return $a + $b;
}
public function divide($a, $b) {
if ($b == 0) {
throw new \InvalidArgumentException("Division by zero");
}
return $a / $b;
}
public function factorial($n) {
if ($n < 0) throw new \InvalidArgumentException("Negative numbers not allowed");
if ($n <= 1) return 1;
return $n * $this->factorial($n - 1);
}
}
测试类
<?php
// tests/Unit/CalculatorTest.php
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase {
private $calculator;
// 在每个测试方法执行前运行
protected function setUp(): void {
$this->calculator = new Calculator();
}
/** @test */
public function testAdd() {
$result = $this->calculator->add(1, 2);
$this->assertEquals(3, $result);
}
/** @test */
public function testAddWithNegativeNumbers() {
$this->assertEquals(-1, $this->calculator->add(2, -3));
$this->assertEquals(0, $this->calculator->add(-5, 5));
}
/** @test */
public function testDivision() {
$this->assertEquals(5, $this->calculator->divide(10, 2));
$this->assertEquals(0.5, $this->calculator->divide(1, 2));
}
/** @test */
public function testDivisionByZeroThrowsException() {
$this->expectException(\InvalidArgumentException::class);
$this->calculator->divide(10, 0);
}
/** @test */
public function testFactorial() {
$this->assertEquals(1, $this->calculator->factorial(0));
$this->assertEquals(1, $this->calculator->factorial(1));
$this->assertEquals(120, $this->calculator->factorial(5));
}
}
常用的断言方法
<?php
class AssertionExampleTest extends TestCase {
/** @test */
public function testCommonAssertions() {
// 相等性断言
$this->assertEquals(5, 2 + 3);
$this->assertSame('5', 2 + 3); // 严格比较(类型+值)
$this->assertNotEquals(4, 2 + 3);
// 布尔值断言
$this->assertTrue(true);
$this->assertFalse(false);
// 空值断言
$this->assertNull(null);
$this->assertNotNull('not null');
// 数组断言
$this->assertCount(3, [1, 2, 3]);
$this->assertContains(2, [1, 2, 3]);
$this->assertArrayHasKey('name', ['name' => 'John']);
// 类型断言
$this->assertInstanceOf(stdClass::class, new stdClass());
$this->assertIsString('hello');
$this->assertIsInt(123);
// 异常断言
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Error message');
}
}
模拟对象(Mock Objects)
使用Mockito风格的模拟
<?php
use PHPUnit\Framework\TestCase;
interface UserRepository {
public function find($id);
public function save($user);
}
class UserService {
private $repository;
public function __construct(UserRepository $repository) {
$this->repository = $repository;
}
public function getUserName($id) {
$user = $this->repository->find($id);
return $user ? $user->getName() : null;
}
}
class UserServiceTest extends TestCase {
/** @test */
public function testGetUserName() {
// 创建模拟对象
$repository = $this->createMock(UserRepository::class);
// 创建模拟用户
$user = $this->createMock(stdClass::class);
$user->method('getName')->willReturn('John Doe');
// 配置模拟行为
$repository->expects($this->once())
->method('find')
->with(1)
->willReturn($user);
$service = new UserService($repository);
$result = $service->getUserName(1);
$this->assertEquals('John Doe', $result);
}
/** @test */
public function testGetUserNameNotFound() {
$repository = $this->createMock(UserRepository::class);
$repository->method('find')->willReturn(null);
$service = new UserService($repository);
$result = $service->getUserName(999);
$this->assertNull($result);
}
}
数据提供器(Data Providers)
<?php
class DataProviderTest extends TestCase {
/**
* @test
* @dataProvider additionProvider
*/
public function testAdd($a, $b, $expected) {
$calculator = new Calculator();
$this->assertEquals($expected, $calculator->add($a, $b));
}
public function additionProvider() {
return [
'positive numbers' => [1, 2, 3],
'negative numbers' => [-1, -2, -3],
'zero' => [0, 0, 0],
'large numbers' => [1000000, 2000000, 3000000],
];
}
}
测试数据库相关代码
<?php
use PHPUnit\Framework\TestCase;
class DatabaseTest extends TestCase {
private static $pdo;
public static function setUpBeforeClass(): void {
// 使用内存数据库进行测试
self::$pdo = new PDO('sqlite::memory:');
self::$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
}
protected function setUp(): void {
// 清理数据以确保测试隔离
self::$pdo->exec('DELETE FROM users');
}
/** @test */
public function testInsertUser() {
$stmt = self::$pdo->prepare('INSERT INTO users (name) VALUES (?)');
$stmt->execute(['John Doe']);
$this->assertEquals(1, self::$pdo->lastInsertId());
$stmt = self::$pdo->query('SELECT * FROM users WHERE id = 1');
$user = $stmt->fetch();
$this->assertEquals('John Doe', $user['name']);
}
}
测试命令
# 运行所有测试 vendor/bin/phpunit # 运行特定测试文件 vendor/bin/phpunit tests/Unit/CalculatorTest.php # 运行特定测试方法 vendor/bin/phpunit --filter testAdd tests/Unit/CalculatorTest.php # 生成代码覆盖率报告 vendor/bin/phpunit --coverage-html coverage # 使用特定配置文件 vendor/bin/phpunit -c phpunit.xml
最佳实践
<?php
/**
* 最佳实践示例
*/
class BestPracticesTest extends TestCase {
// 1. 方法命名清晰
/** @test */
public function it_should_calculate_total_price_with_tax() {
// 测试代码
}
// 2. 使用AAA模式(Arrange-Act-Assert)
/** @test */
public function testCalculateTotal() {
// Arrange
$calculator = new PriceCalculator();
$items = [new Item(10.00), new Item(20.00)];
// Act
$total = $calculator->calculateTotal($items, 0.1);
// Assert
$this->assertEquals(33.00, $total);
}
// 3. 测试边界条件
/** @test */
public function testWithEmptyArray() {
$calculator = new PriceCalculator();
$this->assertEquals(0, $calculator->calculateTotal([], 0.1));
}
// 4. 测试异常情况
/** @test */
public function testWithNegativeTaxRate() {
$this->expectException(\InvalidArgumentException::class);
$calculator = new PriceCalculator();
$calculator->calculateTotal([new Item(10.00)], -0.1);
}
// 5. 使用测试替身隔离依赖
/** @test */
public function testEmailNotification() {
$mailer = $this->createMock(Mailer::class);
$mailer->expects($this->once())
->method('send')
->with($this->stringContains('Welcome'));
$service = new UserRegistrationService($mailer);
$service->register(['email' => 'test@example.com']);
}
}
常见测试模式
测试私有方法和属性
<?php
class PrivateMethodTest extends TestCase {
/** @test */
public function testPrivateMethodUsingReflection() {
$object = new SomeClass();
$reflection = new ReflectionMethod(SomeClass::class, 'privateMethod');
$reflection->setAccessible(true);
$result = $reflection->invoke($object, 'input');
$this->assertEquals('expected_output', $result);
}
}
测试静态方法
<?php
class StaticMethodTest extends TestCase {
/** @test */
public function testStaticMethod() {
$result = SomeClass::staticMethod();
$this->assertEquals('expected', $result);
}
}
编写PHP单元测试的关键点:
- 测试方法命名要清晰:
testSomething或it_should_do_something - 使用AAA模式:Arrange(准备)、Act(执行)、Assert(断言)
- 测试边界条件:空值、零值、负数、大量数据等
- 测试异常情况:确保错误被正确处理
- 使用数据提供器:减少重复代码
- 隔离外部依赖:使用Mock对象
- 保持测试独立:每个测试应该能独立运行
- 关注行为而非实现:测试公共接口而非内部细节
好的测试不仅验证代码正确性,还能作为文档,帮助其他开发者理解你的代码应该如何使用。