本文目录导读:

我来详细讲解如何为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对象
- 保持测试独立:每个测试应该能独立运行
- 关注行为而非实现:测试公共接口而非内部细节
好的测试不仅验证代码正确性,还能作为文档,帮助其他开发者理解你的代码应该如何使用。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播