如何为PHP项目编写单元测试?

wen PHP项目 3

本文目录导读:

如何为PHP项目编写单元测试?

  1. 选择测试框架
  2. 基本项目结构
  3. 配置PHPUnit
  4. 编写第一个测试
  5. 常用的断言方法
  6. 模拟对象(Mock Objects)
  7. 数据提供器(Data Providers)
  8. 测试数据库相关代码
  9. 测试命令
  10. 最佳实践
  11. 常见测试模式

我来详细讲解如何为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单元测试的关键点:

  1. 测试方法命名要清晰testSomethingit_should_do_something
  2. 使用AAA模式:Arrange(准备)、Act(执行)、Assert(断言)
  3. 测试边界条件:空值、零值、负数、大量数据等
  4. 测试异常情况:确保错误被正确处理
  5. 使用数据提供器:减少重复代码
  6. 隔离外部依赖:使用Mock对象
  7. 保持测试独立:每个测试应该能独立运行
  8. 关注行为而非实现:测试公共接口而非内部细节

好的测试不仅验证代码正确性,还能作为文档,帮助其他开发者理解你的代码应该如何使用。

抱歉,评论功能暂时关闭!