在软件开发中,测试是一个非常重要的环节。测试能帮我们发现代码中的bug,保证代码的质量,确保代码按照预期工作。
测试分为很多种:单元测试、集成测试、功能测试、性能测试、安全测试等。其中,单元测试是最基础、最重要的测试。
单元测试,是对软件中最小的可测试单元进行检查和验证。在PHP中,最小的可测试单元通常是一个函数或一个类的方法。单元测试,就是编写代码来测试这些函数或方法,确保它们按照预期工作。
单元测试的好处:
- 保证代码质量:单元测试能帮我们发现代码中的bug,保证代码的正确性。
- 方便重构:有了单元测试,重构代码时就不用担心改坏了功能,只要运行测试,就能知道重构是否正确。
- 文档作用:单元测试本身就是一种文档,通过看测试代码,就能知道函数或方法是如何使用的。
- 提高开发效率:虽然写单元测试需要花时间,但长期来看,单元测试能减少调试和修复bug的时间,提高开发效率。
- 减少回归bug:修改代码后,运行单元测试,就能知道是否引入了新的bug,减少回归bug。
PHPUnit是PHP最流行的单元测试框架,它是xUnit家族的一员(和JUnit、NUnit等类似),提供了丰富的断言和测试功能,能帮我们轻松编写和运行单元测试。
今天分享PHPUnit单元测试的详解,从安装到使用,帮你掌握PHP单元测试。
PHPUnit安装
方法一:Composer安装(推荐)
用Composer安装PHPUnit:
composer require --dev phpunit/phpunit这会把PHPUnit安装到项目的vendor目录中,作为开发依赖。安装完成后,可以用以下命令运行PHPUnit:
vendor/bin/phpunitWindows下:
vendor\bin\phpunit.bat方法二:下载PHAR文件
下载PHPUnit的PHAR文件:
wget https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
mv phpunit.phar /usr/local/bin/phpunit然后就可以直接用phpunit命令运行测试。
验证安装
phpunit --version如果显示PHPUnit的版本号,说明安装成功。
PHPUnit基本使用
1. 编写第一个测试
假设我们有一个简单的函数,需要测试:
// src/Math.php
class Math {
public function add($a, $b) {
return $a + $b;
}
public function subtract($a, $b) {
return $a - $b;
}
}编写测试类:
// tests/MathTest.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 testSubtract() {
$math = new Math();
$result = $math->subtract(5, 3);
$this->assertEquals(2, $result);
}
}测试类的命名规则:
- 测试类继承
PHPUnit\Framework\TestCase - 测试类名通常是
被测试类名 + Test - 测试方法名通常是
test + 被测试方法名,或者以test开头 - 测试文件通常放在
tests目录下
2. 运行测试
phpunit tests/MathTest.php或者运行整个tests目录:
phpunit tests运行结果:
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 00:00.005, Memory: 4.00 MB
OK (2 tests, 2 assertions)两个点表示两个测试都通过了,OK (2 tests, 2 assertions)表示2个测试,2个断言,全部通过。
3. 测试失败
如果测试失败,会显示详细的错误信息:
public function testAdd() {
$math = new Math();
$result = $math->add(2, 3);
$this->assertEquals(6, $result); // 预期6,实际5
}运行结果:
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 00:00.010, Memory: 4.00 MB
There was 1 failure:
1) MathTest::testAdd
Failed asserting that 5 matches expected 6.
/path/to/tests/MathTest.php:8
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.F表示测试失败,下面显示了失败的原因和位置。
常用断言
断言是单元测试的核心,用来验证结果是否符合预期。PHPUnit提供了丰富的断言方法:
1. 相等断言
$this->assertEquals($expected, $actual); // 相等(==)
$this->assertNotEquals($expected, $actual); // 不相等
$this->assertSame($expected, $actual); // 完全相同(===,类型和值都相同)
$this->assertNotSame($expected, $actual); // 不完全相同2. 布尔断言
$this->assertTrue($condition); // 为真
$this->assertFalse($condition); // 为假3. 空值断言
$this->assertNull($value); // 为null
$this->assertNotNull($value); // 不为null
$this->assertEmpty($value); // 为空(null、''、0、'0'、[]、false都算空)
$this->assertNotEmpty($value); // 不为空4. 数值断言
$this->assertGreaterThan($expected, $actual); // 大于
$this->assertGreaterThanOrEqual($expected, $actual); // 大于等于
$this->assertLessThan($expected, $actual); // 小于
$this->assertLessThanOrEqual($expected, $actual); // 小于等于5. 字符串断言
$this->assertStringContainsString($needle, $haystack); // 包含字符串
$this->assertStringStartsWith($prefix, $string); // 以...开头
$this->assertStringEndsWith($suffix, $string); // 以...结尾
$this->assertMatchesRegularExpression($pattern, $string); // 匹配正则表达式
$this->assertStringMatchesFormat($format, $string); // 匹配格式6. 数组断言
$this->assertArrayHasKey($key, $array); // 数组有指定键
$this->assertArrayNotHasKey($key, $array); // 数组没有指定键
$this->assertContains($needle, $array); // 数组包含指定值
$this->assertNotContains($needle, $array); // 数组不包含指定值
$this->assertCount($expectedCount, $array); // 数组元素个数
$this->assertEmpty($array); // 数组为空7. 对象断言
$this->assertInstanceOf($className, $object); // 对象是指定类的实例
$this->assertObjectHasAttribute($attributeName, $object); // 对象有指定属性8. 文件断言
$this->assertFileExists($filename); // 文件存在
$this->assertFileNotExists($filename); // 文件不存在
$this->assertFileEquals($expected, $actual); // 两个文件内容相同9. 异常断言
$this->expectException(\Exception::class); // 预期抛出异常
$this->expectExceptionMessage('错误信息'); // 预期异常消息
$this->expectExceptionCode(100); // 预期异常代码测试夹具(Fixture)
测试夹具,是测试运行前需要准备的环境和数据。PHPUnit提供了setUp()和tearDown()方法,在每个测试方法运行前和运行后执行。
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase {
private $user;
protected function setUp(): void {
// 每个测试方法运行前执行
$this->user = new User();
$this->user->setName('张三');
$this->user->setEmail('zhangsan@example.com');
}
protected function tearDown(): void {
// 每个测试方法运行后执行
$this->user = null;
}
public function testGetName() {
$this->assertEquals('张三', $this->user->getName());
}
public function testGetEmail() {
$this->assertEquals('zhangsan@example.com', $this->user->getEmail());
}
}setUp()方法在每个测试方法运行前执行,用来初始化测试数据;tearDown()方法在每个测试方法运行后执行,用来清理测试数据。
如果需要在所有测试方法运行前只执行一次,可以用setUpBeforeClass()和tearDownAfterClass():
public static function setUpBeforeClass(): void {
// 所有测试方法运行前只执行一次
// 比如创建数据库连接、创建表等
}
public static function tearDownAfterClass(): void {
// 所有测试方法运行后只执行一次
// 比如关闭数据库连接、删除表等
}数据提供器(Data Provider)
数据提供器,可以用一组数据来运行同一个测试方法,避免重复代码。
use PHPUnit\Framework\TestCase;
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注解指定了数据提供器方法,数据提供器返回一个二维数组,每个子数组的元素会作为参数传递给测试方法。这样,testAdd方法会用5组数据运行5次。
测试异常
测试方法是否抛出了预期的异常:
public function testDivideByZero() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('除数不能为0');
$math = new Math();
$math->divide(10, 0);
}如果方法抛出了预期的异常,测试通过;如果没有抛出异常,或者抛出了其他异常,测试失败。
测试输出
测试方法的输出:
public function testOutput() {
$this->expectOutputString('Hello World');
echo 'Hello World';
}expectOutputString()验证方法的输出是否等于预期的字符串。
跳过测试和标记测试
有时候,某些测试在特定条件下需要跳过,或者标记为不完整:
public function testSomething() {
if (!extension_loaded('mysqli')) {
$this->markTestSkipped('需要mysqli扩展');
}
// 测试代码
}
public function testIncomplete() {
$this->markTestIncomplete('这个测试还没写完');
}markTestSkipped()跳过测试,markTestIncomplete()标记测试为不完整。
PHPUnit配置文件
可以用XML配置文件来配置PHPUnit,文件名通常是phpunit.xml:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
verbose="true">
<testsuites>
<testsuite name="Application Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory>src</directory>
</include>
</coverage>
</phpunit>配置文件说明:
bootstrap:测试运行前加载的文件,通常是Composer的autoload.phpcolors:是否显示彩色输出verbose:是否显示详细信息testsuites:测试套件,指定测试文件目录coverage:代码覆盖率配置,指定需要统计覆盖率的目录
有了配置文件,直接运行phpunit命令就会自动读取配置。
代码覆盖率
PHPUnit可以生成代码覆盖率报告,看看测试覆盖了多少代码。
生成HTML格式的覆盖率报告:
phpunit --coverage-html coverage-report这会在coverage-report目录生成HTML格式的覆盖率报告,用浏览器打开index.html就能查看。
注意:代码覆盖率需要安装Xdebug或PCOV扩展。
实战:测试一个用户注册类
假设我们有一个用户注册类,需要测试:
// src/User.php
class User {
private $name;
private $email;
private $password;
public function __construct($name, $email, $password) {
$this->setName($name);
$this->setEmail($email);
$this->setPassword($password);
}
public function setName($name) {
if (empty($name)) {
throw new \InvalidArgumentException('用户名不能为空');
}
if (strlen($name) < 2 || strlen($name) > 20) {
throw new \InvalidArgumentException('用户名长度必须在2-20之间');
}
$this->name = $name;
}
public function setEmail($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('邮箱格式不正确');
}
$this->email = $email;
}
public function setPassword($password) {
if (strlen($password) < 6) {
throw new \InvalidArgumentException('密码长度不能少于6位');
}
$this->password = password_hash($password, PASSWORD_DEFAULT);
}
public function getName() {
return $this->name;
}
public function getEmail() {
return $this->email;
}
public function verifyPassword($password) {
return password_verify($password, $this->password);
}
}编写测试:
// tests/UserTest.php
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase {
public function testValidUserCreation() {
$user = new User('张三', 'zhangsan@example.com', '123456');
$this->assertEquals('张三', $user->getName());
$this->assertEquals('zhangsan@example.com', $user->getEmail());
}
public function testEmptyNameThrowsException() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('用户名不能为空');
new User('', 'test@example.com', '123456');
}
public function testShortNameThrowsException() {
$this->expectException(\InvalidArgumentException::class);
new User('A', 'test@example.com', '123456');
}
public function testLongNameThrowsException() {
$this->expectException(\InvalidArgumentException::class);
new User(str_repeat('A', 21), 'test@example.com', '123456');
}
public function testInvalidEmailThrowsException() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('邮箱格式不正确');
new User('张三', 'invalid-email', '123456');
}
public function testShortPasswordThrowsException() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('密码长度不能少于6位');
new User('张三', 'test@example.com', '12345');
}
public function testPasswordVerification() {
$user = new User('张三', 'test@example.com', '123456');
$this->assertTrue($user->verifyPassword('123456'));
$this->assertFalse($user->verifyPassword('wrongpassword'));
}
/**
* @dataProvider validEmailProvider
*/
public function testValidEmails($email) {
$user = new User('张三', $email, '123456');
$this->assertEquals($email, $user->getEmail());
}
public function validEmailProvider() {
return [
['test@example.com'],
['user.name@domain.co.uk'],
['user+tag@example.org'],
['123456@example.com'],
];
}
}这个测试类,测试了用户注册类的各种情况:正常创建、用户名不能为空、用户名太短/太长、邮箱格式不正确、密码太短、密码验证、合法邮箱等。用数据提供器测试了多个合法邮箱。
单元测试最佳实践
- 测试要独立:每个测试方法应该独立运行,不依赖其他测试的结果,也不依赖运行顺序。
- 测试要快速:单元测试应该快速运行,不要依赖数据库、网络等外部资源。如果需要,用Mock替代。
- 测试要可读:测试代码应该清晰易读,方法名和变量名要有意义。
- 一个测试只测一件事:每个测试方法只测试一个功能点,这样失败时容易定位问题。
- 测试要覆盖边界条件:不仅要测试正常情况,还要测试边界条件和异常情况。
- 先写测试,再写代码(TDD):测试驱动开发,先写测试,再写代码让测试通过,能保证代码的可测试性和质量。
- 定期运行测试:每次修改代码后,都要运行测试,确保没有引入新的bug。
- 保持测试更新:代码修改后,对应的测试也要更新,保持测试和代码同步。
- 不要测试实现细节:测试应该测试行为,而不是实现细节,这样重构时不需要改测试。
- 合理使用Mock:对于依赖外部资源(数据库、API等)的代码,用Mock替代,让测试独立快速。
总结
PHPUnit是PHP最流行的单元测试框架,掌握它能帮你写出更高质量的代码。
核心要点:
- 安装:Composer安装(推荐)或下载PHAR文件
- 基本使用:测试类继承TestCase,测试方法以test开头,用断言验证结果
- 常用断言:assertEquals、assertTrue、assertFalse、assertNull、assertEmpty、assertContains、assertArrayHasKey、assertInstanceOf、expectException等
- 测试夹具:setUp()/tearDown()在每个测试前后执行,setUpBeforeClass()/tearDownAfterClass()在所有测试前后执行
- 数据提供器:用@dataProvider注解,用一组数据运行同一个测试
- 测试异常:用expectException()测试方法是否抛出预期异常
- 配置文件:phpunit.xml配置测试套件、bootstrap、覆盖率等
- 代码覆盖率:用--coverage-html生成覆盖率报告
- 最佳实践:测试独立、快速、可读,一个测试测一件事,覆盖边界条件,TDD,定期运行测试
单元测试,是保证代码质量的重要手段。虽然写单元测试需要花时间,但长期来看,它能减少bug,方便重构,提高开发效率。作为PHP开发者,应该养成写单元测试的习惯,用PHPUnit为你的代码保驾护航。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录