单元测试是保证代码质量的重要手段,它能帮助我们发现代码中的bug,保证代码的正确性,提高代码的可维护性。在PHP开发中,PHPUnit是最流行的单元测试框架,几乎是PHP单元测试的事实标准。今天就来分享PHPUnit的实战教程,帮助大家快速上手PHP单元测试。
为什么要写单元测试
很多开发者觉得写单元测试浪费时间,不如直接写业务代码。但是从长期来看,写单元测试能大大提高开发效率和代码质量。
1. 发现bug:单元测试能在开发阶段就发现代码中的bug,而不是等到上线后才发现,大大降低了修复成本。
2. 保证重构安全:有了单元测试,我们可以放心地重构代码,只要运行测试就能知道重构是否破坏了原有功能。
3. 文档作用:单元测试是最好的文档,通过测试用例可以清楚地了解代码的功能和使用方法。
4. 提高设计质量:为了让代码可测试,我们会自然地写出低耦合、高内聚的代码,提高代码的设计质量。
5. 减少调试时间:有了单元测试,出现问题时可以快速定位到具体的函数或方法,大大减少调试时间。
安装PHPUnit
PHPUnit推荐通过Composer安装:
# 安装PHPUnit
composer require --dev phpunit/phpunit
# 查看版本
vendor/bin/phpunit --version2016年时,PHPUnit的最新稳定版本是PHPUnit 5.x,支持PHP 5.6和PHP 7.0。
第一个测试用例
假设我们有一个简单的计算器类:
<?php
// src/Calculator.php
class Calculator
{
public function add($a, $b)
{
return $a + $b;
}
public function subtract($a, $b)
{
return $a - $b;
}
public function multiply($a, $b)
{
return $a * $b;
}
public function divide($a, $b)
{
if ($b == 0) {
throw new InvalidArgumentException('除数不能为零');
}
return $a / $b;
}
}对应的测试用例:
<?php
// tests/CalculatorTest.php
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
private $calculator;
protected function setUp()
{
$this->calculator = new Calculator();
}
public function testAdd()
{
$result = $this->calculator->add(2, 3);
$this->assertEquals(5, $result);
}
public function testSubtract()
{
$result = $this->calculator->subtract(5, 3);
$this->assertEquals(2, $result);
}
public function testMultiply()
{
$result = $this->calculator->multiply(2, 3);
$this->assertEquals(6, $result);
}
public function testDivide()
{
$result = $this->calculator->divide(6, 3);
$this->assertEquals(2, $result);
}
public function testDivideByZero()
{
$this->expectException(InvalidArgumentException::class);
$this->calculator->divide(6, 0);
}
}运行测试:
vendor/bin/phpunit tests/CalculatorTest.phpPHPUnit核心概念
1. TestCase(测试用例):所有测试类都继承自PHPUnit\Framework\TestCase,测试方法以test开头。
2. setUp()和tearDown():
setUp():每个测试方法执行前调用,用于初始化测试环境tearDown():每个测试方法执行后调用,用于清理测试环境
3. 断言(Assertion):断言是测试的核心,用于验证代码的行为是否符合预期。常用的断言方法:
assertEquals($expected, $actual):断言两个值相等assertSame($expected, $actual):断言两个值完全相同(类型和值)assertTrue($condition):断言条件为真assertFalse($condition):断言条件为假assertNull($value):断言值为nullassertNotNull($value):断言值不为nullassertContains($needle, $haystack):断言数组包含某个元素assertCount($expectedCount, $array):断言数组长度assertInstanceOf($expected, $actual):断言对象是某个类的实例expectException($exception):断言会抛出某个异常
4. 数据提供器(Data Provider):用于为同一个测试方法提供多组测试数据:
/**
* @dataProvider additionProvider
*/
public function testAdd($a, $b, $expected)
{
$this->assertEquals($expected, $this->calculator->add($a, $b));
}
public function additionProvider()
{
return [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 2],
[2, 3, 5],
[-1, 1, 0],
];
}5. 测试替身(Test Double):
- Stub(桩对象):替换依赖,返回预设值
- Mock(模拟对象):验证方法是否被调用,调用参数和次数
// Stub示例
public function testStub()
{
$stub = $this->createMock(SomeClass::class);
$stub->method('doSomething')
->willReturn('foo');
$this->assertEquals('foo', $stub->doSomething());
}
// Mock示例
public function testMock()
{
$mock = $this->createMock(SomeClass::class);
$mock->expects($this->once())
->method('doSomething')
->with($this->equalTo('param'));
$mock->doSomething('param');
}配置文件phpunit.xml
PHPUnit支持通过XML配置文件来管理测试:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
stopOnFailure="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory>tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">src/</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="build/coverage"/>
<log type="junit" target="build/logfile.xml"/>
</logging>
</phpunit>配置好后,直接运行vendor/bin/phpunit即可执行所有测试。
代码覆盖率
PHPUnit支持生成代码覆盖率报告,需要安装Xdebug扩展:
# 生成HTML格式的覆盖率报告
vendor/bin/phpunit --coverage-html build/coverage
# 生成文本格式的覆盖率报告
vendor/bin/phpunit --coverage-text代码覆盖率能帮助我们发现哪些代码没有被测试覆盖到,从而补充测试用例。但是要注意,100%的代码覆盖率不代表代码没有bug,它只是说明所有代码都被执行过了。
实战:测试数据库操作
数据库操作的测试比较复杂,通常需要使用测试数据库或Mock。这里介绍使用SQLite内存数据库进行测试的方法:
<?php
// src/UserRepository.php
class UserRepository
{
private $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
public function findById($id)
{
$stmt = $this->db->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function create($name, $email)
{
$stmt = $this->db->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->execute([$name, $email]);
return $this->db->lastInsertId();
}
}<?php
// tests/UserRepositoryTest.php
use PHPUnit\Framework\TestCase;
class UserRepositoryTest extends TestCase
{
private $db;
private $repository;
protected function setUp()
{
// 使用SQLite内存数据库
$this->db = new PDO('sqlite::memory:');
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 创建表
$this->db->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)');
$this->repository = new UserRepository($this->db);
}
public function testCreate()
{
$id = $this->repository->create('张三', 'zhangsan@example.com');
$this->assertNotEmpty($id);
$user = $this->repository->findById($id);
$this->assertEquals('张三', $user['name']);
$this->assertEquals('zhangsan@example.com', $user['email']);
}
public function testFindByIdNotFound()
{
$user = $this->repository->findById(999);
$this->assertFalse($user);
}
}单元测试最佳实践
1. 测试命名清晰:测试方法名应该描述测试的场景和预期结果,比如testAddPositiveNumbers、testDivideByZeroThrowsException。
2. 一个测试只测一件事:每个测试方法应该只验证一个行为,这样失败时能快速定位问题。
3. 测试独立:测试之间不应该有依赖,每个测试都应该能独立运行,运行顺序不影响结果。
4. 测试快速:单元测试应该快速运行,避免依赖外部资源(数据库、网络、文件系统),使用Mock或Stub替换依赖。
5. 测试可读:测试代码应该和业务代码一样清晰易读,使用有意义的变量名,添加必要的注释。
6. 先写测试(TDD):测试驱动开发(Test-Driven Development),先写测试,再写代码,能保证代码的可测试性和正确性。
7. 持续集成:将单元测试集成到CI流程中,每次提交代码都自动运行测试,保证代码质量。
总结
PHPUnit是PHP单元测试的标准框架,掌握PHPUnit能大大提高我们的代码质量和开发效率。单元测试不是浪费时间,而是投资,它能在长期带来丰厚的回报。
希望这篇教程能帮助大家快速上手PHP单元测试,写出更高质量的代码。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录