作为PHP开发者,你有没有过这样的经历:改了一个功能,结果另一个地方出bug了;上线了一个新功能,用户反馈说老功能不能用了;排查一个bug,花了半天时间,最后发现是一个很小的改动引起的连锁反应。
这些问题,很多时候都是因为没有测试导致的。我们写完代码,手动测一下觉得没问题就上线了,但手动测试覆盖不了所有的场景,很容易遗漏。而且代码是不断迭代的,今天改了这里,明天改了那里,谁也不能保证改动不会影响其他地方。
单元测试就是为了解决这些问题而生的。单元测试是对代码中最小的可测试单元(函数、方法、类)进行验证,确保它们在各种情况下都能正确工作。有了单元测试,我们可以:
- 保证代码质量:每个单元都经过测试,减少bug
- 放心重构:重构代码后,运行测试就能知道有没有破坏原有功能
- 文档作用:测试用例就是最好的文档,看测试就知道代码怎么用
- 提升设计:为了让代码可测试,我们会写出更松耦合、更易维护的代码
- 减少调试时间:出了问题,测试能快速定位是哪个单元的问题
PHPUnit是PHP最流行的单元测试框架,几乎是PHP单元测试的标准。今天分享PHPUnit的入门教程,帮你从零开始学会写单元测试。
安装PHPUnit
安装PHPUnit最简单的方式是用Composer:
composer require --dev phpunit/phpunit ^5.0安装完成后,vendor/bin目录下会有phpunit可执行文件。运行:
vendor/bin/phpunit --version如果显示版本号,说明安装成功。
也可以下载phpunit.phar全局使用:
wget https://phar.phpunit.de/phpunit-5.7.phar
chmod +x phpunit-5.7.phar
sudo mv phpunit-5.7.phar /usr/local/bin/phpunit
phpunit --version配置文件
在项目根目录创建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>
</phpunit>关键配置:
- bootstrap:测试启动时加载的文件,一般是Composer的autoload
- colors:终端输出是否带颜色
- testsuites:测试套件,指定测试文件目录
- whitelist:代码覆盖率白名单,指定要统计覆盖率的源码目录
第一个测试
假设我们有一个简单的计算器类,src/Calculator.php:
<?php
namespace App;
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('Division by zero');
}
return $a / $b;
}
}对应的测试文件,tests/CalculatorTest.php:
<?php
use PHPUnit\Framework\TestCase;
use App\Calculator;
class CalculatorTest extends TestCase
{
public function testAdd()
{
$calc = new Calculator();
$result = $calc->add(2, 3);
$this->assertEquals(5, $result);
}
public function testSubtract()
{
$calc = new Calculator();
$result = $calc->subtract(5, 3);
$this->assertEquals(2, $result);
}
public function testMultiply()
{
$calc = new Calculator();
$result = $calc->multiply(4, 3);
$this->assertEquals(12, $result);
}
public function testDivide()
{
$calc = new Calculator();
$result = $calc->divide(10, 2);
$this->assertEquals(5, $result);
}
public function testDivideByZero()
{
$this->expectException(\InvalidArgumentException::class);
$calc = new Calculator();
$calc->divide(10, 0);
}
}测试类的命名规范:
- 测试类继承PHPUnit\Framework\TestCase
- 测试类名以Test结尾
- 测试方法名以test开头,或者用/* @test /注解
- 测试文件放在tests目录,文件名以Test结尾
运行测试:
vendor/bin/phpunit输出:
PHPUnit 5.7.27 by Sebastian Bergmann and contributors.
..... 5 / 5 (100%)
Time: 100 ms, Memory: 4.00MB
OK (5 tests, 5 assertions)5个测试全部通过!
常用断言方法
PHPUnit提供了丰富的断言方法,常用的有:
assertEquals($expected, $actual):断言两个值相等(==比较)assertSame($expected, $actual):断言两个值完全相同(===比较,类型也相同)assertTrue($condition):断言条件为真assertFalse($condition):断言条件为假assertNull($value):断言值为nullassertNotNull($value):断言值不为nullassertContains($needle, $haystack):断言数组包含某个元素assertCount($expected, $array):断言数组元素个数assertEmpty($value):断言值为空assertNotEmpty($value):断言值不为空assertInstanceOf($expected, $actual):断言对象是某个类的实例expectException($exception):断言会抛出某个异常assertRegExp($pattern, $string):断言字符串匹配正则assertGreaterThan($expected, $actual):断言实际值大于期望值assertLessThan($expected, $actual):断言实际值小于期望值
合理使用断言,能让测试更精确、更有意义。
setUp和tearDown
如果每个测试方法都需要创建相同的对象,可以用setUp方法在每个测试前初始化:
class CalculatorTest extends TestCase
{
private $calc;
protected function setUp()
{
$this->calc = new Calculator();
}
protected function tearDown()
{
$this->calc = null;
}
public function testAdd()
{
$result = $this->calc->add(2, 3);
$this->assertEquals(5, $result);
}
// ...
}- setUp():每个测试方法执行前调用,用于初始化测试数据
- tearDown():每个测试方法执行后调用,用于清理资源
- setUpBeforeClass():整个测试类执行前调用一次(静态方法)
- tearDownAfterClass():整个测试类执行后调用一次(静态方法)
数据提供者
如果同一个测试需要用多组数据测试,可以用数据提供者(Data Provider):
class CalculatorTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd($a, $b, $expected)
{
$calc = new Calculator();
$this->assertEquals($expected, $calc->add($a, $b));
}
public function additionProvider()
{
return [
[0, 0, 0],
[1, 1, 2],
[2, 3, 5],
[-1, 1, 0],
[100, -50, 50],
];
}
}数据提供者是一个返回数组的方法,每个子数组是一组测试数据,会作为参数传给测试方法。用数据提供者可以用一组测试方法测试多组数据,减少重复代码。
Mock对象
测试依赖其他类的代码时,可以用Mock对象模拟依赖,隔离测试单元。
假设我们有一个UserService,依赖UserRepository:
class UserService
{
private $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public function getUser($id)
{
$user = $this->repository->find($id);
if (!$user) {
throw new \RuntimeException('User not found');
}
return $user;
}
}测试时用Mock模拟UserRepository:
class UserServiceTest extends TestCase
{
public function testGetUser()
{
$user = ['id' => 1, 'name' => 'Tom'];
$mockRepo = $this->createMock(UserRepository::class);
$mockRepo->method('find')
->with(1)
->willReturn($user);
$service = new UserService($mockRepo);
$result = $service->getUser(1);
$this->assertEquals($user, $result);
}
public function testGetUserNotFound()
{
$this->expectException(\RuntimeException::class);
$mockRepo = $this->createMock(UserRepository::class);
$mockRepo->method('find')->willReturn(null);
$service = new UserService($mockRepo);
$service->getUser(999);
}
}Mock对象能让我们只测试当前单元,不依赖外部资源(数据库、API等),测试更快、更稳定。
代码覆盖率
PHPUnit可以生成代码覆盖率报告,看看测试覆盖了多少代码。
需要安装Xdebug或phpdbg扩展。运行:
vendor/bin/phpunit --coverage-html coverage-report会在coverage-report目录生成HTML格式的覆盖率报告,用浏览器打开index.html就能看到详细的覆盖率数据,包括哪些行被测试覆盖了,哪些没有。
代码覆盖率不是越高越好,但它能帮我们发现没有被测试到的代码。一般来说,核心业务逻辑的覆盖率应该比较高。
最佳实践
- 测试要独立:每个测试方法不依赖其他测试的结果,测试之间不能有状态共享
- 测试要快速:单元测试应该很快就能跑完,不要在单元测试里连数据库、调API
- 测试要有意义:不要为了覆盖率而写无意义的测试,每个测试都应该验证一个具体的行为
- 测试要易读:测试代码也是代码,要写得清晰、易读,方法名和变量名要有意义
- 先写测试还是先写代码:TDD(测试驱动开发)是先写测试再写代码,但不强制,关键是要有测试
- 持续集成:把测试加入持续集成流程,每次提交自动运行测试,及时发现问题
- 测试也要维护:代码重构时,测试也要跟着更新,保持测试的有效性
总结
单元测试是保证代码质量的重要手段,PHPUnit是PHP单元测试的标准框架。掌握PHPUnit的基本用法——安装配置、编写测试、断言方法、setUp/tearDown、数据提供者、Mock对象、代码覆盖率——能帮你写出更可靠、更易维护的代码。
写单元测试一开始可能会觉得麻烦、费时间,但长期来看,它能帮你减少bug、放心重构、提升代码质量。如果你还没有写单元测试的习惯,强烈建议你从今天开始,给你的代码加上测试。
希望这篇入门教程能帮你走上单元测试之路。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录