PHP测试驱动开发与PHPUnit实践
测试是保证代码质量的重要手段。PHPUnit是PHP最流行的测试框架,今天从基础到高级用法都说一遍。
先安装PHPUnit。用Composer安装很方便,composer require --dev phpunit/phpunit。写一个最简单的测试类:
```php
use PHPUnit\Framework\TestCase;
class MathTest extends TestCase
{
public function testAddition(): void
{
$result = 1 + 1;
$this->assertEquals(2, $result);
}
public function testStringConcatenation(): void
{
$result = 'Hello' . ' ' . 'World';
$this->assertEquals('Hello World', $result);
}
public function testArrayPush(): void
{
$arr = [];
array_push($arr, 'item');
$this->assertCount(1, $arr);
$this->assertContains('item', $arr);
}
}
?>
```
测试真实业务类的时候,需要用到setUp方法初始化测试环境。
```php
// 被测试的类
class Calculator
{
public function add(float $a, float $b): float
{
return $a + $b;
}
public function subtract(float $a, float $b): float
{
return $a - $b;
}
public function multiply(float $a, float $b): float
{
return $a * $b;
}
public function divide(float $a, float $b): float
{
if ($b === 0.0) {
throw new InvalidArgumentException('除数不能为0');
}
return $a / $b;
}
public function factorial(int $n): int
{
if ($n < 0) {
throw new InvalidArgumentException('负数没有阶乘');
}
if ($n <= 1) return 1;
return $n * $this->factorial($n - 1);
}
public function isPrime(int $n): bool
{
if ($n < 2) return false;
for ($i = 2; $i <= sqrt($n); $i++) {
if ($n % $i === 0) return false;
}
return true;
}
}
class CalculatorTest extends TestCase
{
private Calculator $calculator;
protected function setUp(): void
{
$this->calculator = new Calculator();
}
public function testAdd(): void
{
$this->assertEquals(5, $this->calculator->add(2, 3));
$this->assertEquals(0, $this->calculator->add(-2, 2));
$this->assertEquals(5.5, $this->calculator->add(2.5, 3.0));
}
public function testDivide(): void
{
$this->assertEquals(2, $this->calculator->divide(10, 5));
$this->assertEquals(3.33, $this->calculator->divide(10, 3), '', 0.01);
}
public function testDivideByZero(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('除数不能为0');
$this->calculator->divide(10, 0);
}
public function testFactorial(): void
{
$this->assertEquals(1, $this->calculator->factorial(0));
$this->assertEquals(1, $this->calculator->factorial(1));
$this->assertEquals(2, $this->calculator->factorial(2));
$this->assertEquals(120, $this->calculator->factorial(5));
}
public function testFactorialNegative(): void
{
$this->expectException(InvalidArgumentException::class);
$this->calculator->factorial(-1);
}
public function testIsPrime(): void
{
$this->assertTrue($this->calculator->isPrime(2));
$this->assertTrue($this->calculator->isPrime(3));
$this->assertFalse($this->calculator->isPrime(4));
$this->assertTrue($this->calculator->isPrime(5));
$this->assertFalse($this->calculator->isPrime(9));
$this->assertTrue($this->calculator->isPrime(11));
$this->assertFalse($this->calculator->isPrime(1));
}
}
?>
```
数据提供器可以用不同的参数多次执行同一个测试:
```php
class DataProviderTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd(int $a, int $b, int $expected): void
{
$result = (new Calculator())->add($a, $b);
$this->assertEquals($expected, $result);
}
public static function additionProvider(): array
{
return [
'正数相加' => [1, 2, 3],
'负数相加' => [-1, -2, -3],
'正负相加' => [5, -3, 2],
'零相加' => [0, 5, 5],
'大数相加' => [1000000, 2000000, 3000000],
];
}
/**
* @dataProvider primeProvider
*/
public function testIsPrime(int $number, bool $expected): void
{
$this->assertEquals($expected, (new Calculator())->isPrime($number));
}
public static function primeProvider(): array
{
return [
[2, true],
[3, true],
[4, false],
[5, true],
[6, false],
[7, true],
[8, false],
[9, false],
[11, true],
[13, true],
];
}
}
?>
```
测试依赖注入和模拟对象:
```php
interface MailerInterface
{
public function send(string $to, string $subject, string $body): bool;
}
class UserService2
{
private MailerInterface $mailer;
private array $users = [];
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
public function register(string $name, string $email): array
{
$user = [
'id' => count($this->users) + 1,
'name' => $name,
'email' => $email,
'created_at' => new DateTime(),
];
$this->users[] = $user;
$this->mailer->send($email, '欢迎注册', "Hello $name!");
return $user;
}
public function getUser(int $id): ?array
{
foreach ($this->users as $user) {
if ($user['id'] === $id) return $user;
}
return null;
}
}
class UserServiceTest extends TestCase
{
public function testRegister(): void
{
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->once())
->method('send')
->with(
$this->equalTo('user@test.com'),
$this->stringContains('欢迎'),
$this->anything()
)
->willReturn(true);
$service = new UserService2($mailer);
$user = $service->register('张三', 'user@test.com');
$this->assertArrayHasKey('id', $user);
$this->assertEquals('张三', $user['name']);
$this->assertEquals('user@test.com', $user['email']);
}
public function testGetUser(): void
{
$mailer = $this->createMock(MailerInterface::class);
$mailer->method('send')->willReturn(true);
$service = new UserService2($mailer);
$service->register('张三', 'a@test.com');
$service->register('李四', 'b@test.com');
$user = $service->getUser(1);
$this->assertNotNull($user);
$this->assertEquals('张三', $user['name']);
$this->assertNull($service->getUser(999));
}
}
?>
```
测试有副作用的代码需要模拟外部依赖:
```php
class DatabaseRepository
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function findUser(int $id): ?array
{
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ?: null;
}
public function saveUser(string $name, string $email): int
{
$stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->execute([$name, $email]);
return (int)$this->pdo->lastInsertId();
}
}
class DatabaseRepositoryTest extends TestCase
{
public function testFindUser(): void
{
$pdo = $this->createMock(PDO::class);
$stmt = $this->createMock(PDOStatement::class);
$pdo->method('prepare')->willReturn($stmt);
$stmt->method('execute')->willReturn(true);
$stmt->method('fetch')->willReturn([
'id' => 1,
'name' => '张三',
'email' => 'test@test.com',
]);
$repo = new DatabaseRepository($pdo);
$user = $repo->findUser(1);
$this->assertNotNull($user);
$this->assertEquals('张三', $user['name']);
}
public function testFindUserNotFound(): void
{
$pdo = $this->createMock(PDO::class);
$stmt = $this->createMock(PDOStatement::class);
$pdo->method('prepare')->willReturn($stmt);
$stmt->method('execute')->willReturn(true);
$stmt->method('fetch')->willReturn(false);
$repo = new DatabaseRepository($pdo);
$this->assertNull($repo->findUser(999));
}
}
?>
PHPUnit的常用断言:
- assertEquals: 检查是否相等
- assertSame: 检查是否全等(===)
- assertNull/assertNotNull: 检查null
- assertTrue/assertFalse: 检查布尔值
- assertCount: 检查数组长度
- assertContains: 检查是否包含
- assertEmpty: 检查是否为空
- assertInstanceOf: 检查类型
测试不只是为了找bug,更重要的是让你敢重构。有测试覆盖的代码,改起来底气足很多。
PHP测试驱动开发与PHPUnit实践
张小明
前端开发工程师
最新点餐源码系统小程序:从单体到Serverless架构升级指南(附代码)
餐饮行业的数字化浪潮正以摧枯拉朽之势席卷而来。扫码点餐、自助结算、私域会员运营——这些曾经的"锦上添花",如今已成为餐饮门店的生存刚需。而支撑这一切的技术底座,正在经历一场深刻的架构革命:从笨重的单体应用,一…
别再手动敲Git命令了!用Pycharm 2023.3的图形化界面搞定版本控制(附GitHub配置)
告别命令行恐惧:PyCharm 2023.3图形化Git全攻略每次在终端输入git commit -m "fix bug"时都要反复检查拼写?面对git rebase -i HEAD~3这样的命令感到头皮发麻?作为Python开发者,其实你完全可以在熟悉的PyCharm环境中&am…
滴滴面试官: 你说熟悉 Agent 记忆机制?那向量库召回不到隐藏因果,你怎么补?长短期记忆怎么拆?我当场沉默了
我的朋友去面滴滴的网约车策略开发岗,面试完后正好在群里找我吐槽了一下,说当场被面试官问得直接没答上来。听完他整个面试过程,我特别有共鸣,也刚好借这件事,跟大家唠唠Agent记忆这块很多人都容易踩的误区和盲区。 他…
前后端分离二手商城开发,质检登记、回收回款整套业务源码部署教程
如今二手物品循环交易市场持续成熟,个人闲置售卖、专业物品回收的需求日益增多。传统二手交易模式缺乏标准化流程,物品真伪、成色无官方核验,私下交易回款无记录、售后无保障,个人交易纠纷频发,小型回收商家也存在质检…
数据预处理实战:分层防御架构与缺失/异常值决策树
1. 这不是教科书里的“数据清洗”,而是一线工程师每天在Excel、SQL和Python里反复擦汗的真实战场“From Raw to Refined: A Journey Through Data Preprocessing — Part 1”——这个标题乍看像学术论文的副标题,但如果你真在银行风控团队跑过模型上线前…
鸿蒙开发--CANNKit-AscendC-sobel
HarmonyOS AscendC 算子:用 NPU 实现图像边缘检测 什么是 AscendC 算子 前面我们介绍了很多图形渲染相关的技术,这篇来看看 AI 领域的东西。AscendC 是华为提供的一种 NPU(神经网络处理器)编程框架,让你可以自己写算子…