很多PHP开发者觉得写单元测试是浪费时间——需求变更快,代码写完就不错了,哪有时间写测试?而且觉得测试很麻烦,要学新工具、写很多额外的代码。
但实际上,单元测试是保证代码质量、提升开发效率的重要手段。我以前也不写测试,觉得"代码能跑就行"。但随着项目越来越大,改代码越来越没有信心——改一个地方,不知道会不会影响其他地方,每次上线都提心吊胆。后来开始写单元测试,发现好处很多:
第一,发现bug。写测试的过程,就是重新审视代码逻辑的过程。很多时候,写测试的时候会发现自己之前忽略的边界条件和异常情况,从而发现潜在的bug。
第二,放心重构。有了单元测试,重构代码就有了保障——改完代码,跑一遍测试,如果测试都通过,说明功能没有被破坏。没有测试的重构,就是赌博。
第三,文档作用。好的单元测试,就是最好的文档——看测试用例,就知道函数/方法的输入输出、边界条件、异常处理。比写文档更准确、更及时。
第四,提升设计。写单元测试会迫使你写出更容易测试的代码——低耦合、高内聚、依赖注入、接口抽象。这些都是好的设计原则。写测试的过程,也是提升代码设计的过程。
PHPUnit是PHP最流行的单元测试框架,几乎是PHP单元测试的事实标准。今天就来分享PHPUnit的入门和实战。
安装PHPUnit
PHPUnit的安装很简单,推荐用Composer安装:
composer require --dev phpunit/phpunit安装完成后,在项目根目录下运行:
vendor/bin/phpunit --version如果显示版本号,说明安装成功。
也可以下载PHPUnit的PHAR包,全局安装:
wget https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
phpunit --version第一个测试
先写一个简单的函数,然后为它写单元测试。
被测试的代码(src/Math.php):
<?php
class Math
{
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;
}
}测试代码(tests/MathTest.php):
<?php
use PHPUnit\Framework\TestCase;
class MathTest extends TestCase
{
public function testAdd()
{
$math = new Math();
$result = $math->add(2, 3);
$this->assertEquals(5, $result);
}
public function testDivide()
{
$math = new Math();
$result = $math->divide(10, 2);
$this->assertEquals(5, $result);
}
public function testDivideByZero()
{
$this->expectException(InvalidArgumentException::class);
$math = new Math();
$math->divide(10, 0);
}
}测试类要继承TestCase,测试方法要以test开头。在测试方法中,用断言(assertion)来验证结果是否符合预期。
运行测试:
vendor/bin/phpunit tests/MathTest.php如果测试通过,会显示OK和测试数量、时间。如果有测试失败,会显示哪个测试失败了、期望的值和实际的值。
常用断言
PHPUnit提供了丰富的断言方法,常用的有:
- assertEquals($expected, $actual):判断两个值是否相等(==比较)
- assertSame($expected, $actual):判断两个值是否完全相同(===比较,类型和值都要相同)
- assertTrue($value):判断值是否为true
- assertFalse($value):判断值是否为false
- assertNull($value):判断值是否为null
- assertNotNull($value):判断值是否不为null
- assertEmpty($value):判断值是否为空(empty())
- assertNotEmpty($value):判断值是否不为空
- assertCount($expected, $array):判断数组长度是否等于期望值
- assertContains($needle, $haystack):判断数组是否包含某个值
- assertArrayHasKey($key, $array):判断数组是否有某个键
- assertStringContainsString($needle, $string):判断字符串是否包含子串
- expectException($exception):期望抛出某个异常
- expectExceptionMessage($message):期望异常消息包含某个字符串
合理使用断言,能让测试更准确、更易读。
测试固件(Fixture)
很多测试需要在测试前准备数据,测试后清理数据。PHPUnit提供了setUp()和tearDown()方法,在每个测试方法前后自动调用。
class UserTest extends TestCase
{
private $user;
protected function setUp(): void
{
// 每个测试前创建一个新的User对象
$this->user = new User();
}
protected function tearDown(): void
{
// 每个测试后清理
$this->user = null;
}
public function testSetName()
{
$this->user->setName("张三");
$this->assertEquals("张三", $this->user->getName());
}
}setUp()在每个测试方法前调用,用来初始化测试数据;tearDown()在每个测试方法后调用,用来清理数据。这样每个测试都是独立的,不会互相影响。
如果所有测试共享同一个初始化,可以用setUpBeforeClass()和tearDownAfterClass(),在整个测试类的前后只调用一次。
数据提供者(Data Provider)
有时候,同一个测试逻辑需要测试多组数据。可以用数据提供者(Data Provider)来批量提供测试数据,避免写重复的测试方法。
class MathTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd($a, $b, $expected)
{
$math = new Math();
$this->assertEquals($expected, $math->add($a, $b));
}
public function additionProvider()
{
return [
[0, 0, 0],
[1, 1, 2],
[2, 3, 5],
[-1, 1, 0],
[100, -50, 50],
];
}
}用@dataProvider注解指定数据提供者方法,数据提供者返回一个二维数组,每个子数组是一组测试数据,会作为参数传给测试方法。这样一个测试方法就能测试多组数据,代码更简洁。
测试覆盖率
测试覆盖率(Code Coverage)衡量测试覆盖了多少代码。PHPUnit可以生成测试覆盖率报告,显示哪些代码被测试覆盖了,哪些没有。
需要安装Xdebug或PCOV扩展来收集覆盖率数据。然后运行:
vendor/bin/phpunit --coverage-html coverage-report tests/这会在coverage-report目录下生成HTML格式的覆盖率报告,用浏览器打开就能看到详细的覆盖率数据,包括每个文件、每个类、每个方法的覆盖率,以及哪些行被执行了、哪些没有。
测试覆盖率不是越高越好,但能帮你发现哪些代码没有被测试到,从而补充测试。一般来说,核心业务逻辑的覆盖率应该比较高(80%以上),而一些简单的getter/setter、配置代码可以不用测试。
写好单元测试的建议
第一,测试要独立。每个测试方法应该是独立的,不依赖其他测试的执行顺序和结果。用setUp()初始化数据,确保每个测试都从相同的状态开始。
第二,测试要可读。测试代码也是代码,要写得清晰、易读。测试方法名要描述清楚测试的意图(如testAddPositiveNumbers、testDivideByZeroThrowsException),让人一看就知道测试什么。
第三,一个测试只测一件事。每个测试方法应该只验证一个行为,不要在一个测试里验证很多东西。如果测试失败,能快速定位是哪里出了问题。
第四,测试边界条件。除了正常情况,还要测试边界条件和异常情况——空值、零、负数、最大值、非法输入、网络异常等。bug往往出现在边界条件。
第五,不要测试实现细节。测试应该关注行为和结果,而不是实现细节。比如测试一个排序方法,应该验证排序后的结果是否正确,而不是验证它用了冒泡排序还是快速排序。这样重构实现时,测试不需要改。
第六,先写测试还是先写代码?测试驱动开发(TDD)主张先写测试,再写代码,让测试来驱动设计。但对于初学者,可以先写代码,再补测试,慢慢养成写测试的习惯。重要的是要有测试,而不是纠结顺序。
总结
单元测试是保证代码质量的重要手段,PHPUnit是PHP最流行的单元测试框架。写单元测试并不难,难的是养成写测试的习惯。
刚开始写测试可能会觉得慢、麻烦,但随着项目越来越大,测试的价值会越来越明显——改代码有信心,重构有保障,上线不担心。从今天开始,为你的核心代码写单元测试吧。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录