PHP单元测试 (一) 基础

概述

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。

测试DEMO

  • Transfer.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

namespace Controller;

class Transfer
{
private $accountA = 100;
private $accountB = 100;

public function aToB(int $money)
{
$this->accountA -= $money;
$this->accountB += $money;
}

public function bToA(int $money)
{
$this->accountB -= $money;
$this->accountA += $money;
}

public function getAccountA()
{
return $this->accountA;
}

public function getAccountB()
{
return $this->accountB;
}
}
  • TransferTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

namespace Test;

use Controller\Transfer;
use PHPUnit\Framework\TestCase;

class TransferTest extends TestCase
{
private $transferObj;

public function setUp() : void
{
$this->transferObj = new Transfer();
}

public function testAtoB()
{
$originalA = $this->transferObj->getAccountA();
$originalB = $this->transferObj->getAccountB();

$this->transferObj->aToB(10);
$this->assertEquals($originalA - 10, $this->transferObj->getAccountA());
$this->assertEquals($originalB + 10, $this->transferObj->getAccountB());
}
}
  • 执行测试:
1
2
3
4
5
6
7
PHPUnit 9.5-gd3b55c36f by Sebastian Bergmann and contributors.

. 1 / 1 (100%)

Time: 00:00.029, Memory: 8.00 MB

OK (1 test, 2 assertions)

详细使用

基境

https://phpunit.readthedocs.io/en/9.3/fixtures.html

PHPUnit 支持共享建立基境的代码。

提供了以下几个模板方法:

  • setUpBeforeClass: 测试用例类的第一个测试运行之前执行
  • tearDownAfterClass: 测试用例类的最后一个测试运行之后执行
  • setUp: 每个测试方法运行之前执行
  • tearDown: 每个测试方法运行之后执行

注意:每个测试方法都是在一个全新的测试类实例上运行的

全局状态

https://phpunit.readthedocs.io/en/9.3/fixtures.html#global-state

  • 全局变量:有时候测试代码中用到了全局变量($_GLOBALS),但是如果对这里面的变量进行了修改,可能会导致其他测试方法出现问题,那么怎么保证每个测试方法都使用的是一样的全局变量呢? 通过:@backupGlobals disabled|enabled 它可标注在:
    • 测试类: 作用范围为整个测试类
    • 测试方法: 作用范围为这个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Test;

use PHPUnit\Framework\TestCase;

/**
* @backupGlobals disabled
*/
class MyTest extends TestCase
{
/**
* @backupGlobals enabled
*/
public function testThatInteractsWithGlobalVariables()
{

}
}

支持设置 “全局变量黑名单” 黑名单中的全局变量将被排除于备份与还原操作之外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @backupGlobals disabled
*/
class MyTest extends TestCase
{
protected $backupGlobalsBlacklist = ['globalVariable'];

/**
* @backupGlobals enabled
*/
public function testThatInteractsWithGlobalVariables()
{

}
}

对于全局变量的备份和还原的原理是使用了:serialize()unserialize()

  • 注意

    • 对于无法被序列化的对象放入 $GLOBALS 数组内时,备份操作就会出问题。比如:PDO
    • 在方法(例如 setUp())内对 $backupGlobalsBlacklist 属性进行设置是无效的
  • 类的静态属性 。对于类的静态属性的备份和还原可以通过:@backupStaticAttributes enabled|disabled
    作用对象:在测试开始时已声明的所有类(而不仅是测试类自身),且只作用于静态类属性,不作用于函数内声明的静态变量。
    使用位置和 backupGlobals 一致:

    • 测试类
    • 测试方法
      只有启用了 @backupStaticAttributes 的测试方法才会在方法之前执行此操作。如果在此之前运行的某个没有启用 @backupStaticAttributes 的测试方法改变了静态属性的值,那么被备份及还原的将会是这个改变后的值

同样提供了黑名单支持:

1
2
3
4
5
6
7
8
class MyTest extends TestCase
{
protected $backupStaticAttributesBlacklist = [
'className' => ['attributeName']
];

// ...
}

依赖关系

使用 @depends 声明测试方法所依赖的其他测试方法。 依赖方法的返回值,会作为被依赖方法的参数,其顺序和 @depends 的顺序一致,但是不会影响代码的执行顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function testOne()
{
$this->assertTrue(true);

return "depends1";
}

public function testTwo()
{
$this->assertTrue(true);
return "depends2";
}

/**
* @depends testOne
* @depends testTwo
*/
public function testDepends()
{
$this->assertEquals(['depends1', 'depends2'], func_get_args());
}
  • 测试结果
1
2
3
Time: 00:00.315, Memory: 6.00 MB

OK (3 tests, 3 assertions)
  • 注意
    • 当被依赖的测试方法失败时,不会再执行依赖方法的测试。
    • 如果被依赖方法返回的是对象,默认是引用传递,如果希望传递对象的副本时,使用: @depends clone

数据供给器

使用 @dataProvider 声明数据供给器。 对应的方法需要返回:

  • 数组(每个元素也是数组)
  • 可遍历的对象(实现了迭代接口)

然后测试时,会将每次迭代器提供的一组数据进行测试,直到全部遍历完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @param $a
* @param $b
* @param $sum
* @dataProvider additionProvider
*/
public function testSum($a, $b, $sum)
{
$this->assertEquals($sum, $a + $b);
}

public function additionProvider()
{
return [
[1, 3, 4],
[1, 1, 2],
[1, 1, 3],
];
}
  • 测试结果
1
2
3
4
5
6
7
8
9
10
11
12
Failed asserting that 2 matches expected 3.
Expected :3
Actual :2
<Click to see difference>

/Users/caoxl/WWW/test.com/tests/MyTest.php:56

Time: 00:00.403, Memory: 8.00 MB


FAILURES!
Tests: 3, Assertions: 3, Failures: 1.
  • 注意:
    • @depends 同时使用时,@provider 提供的参数会优先于 @depends 提供的参数,并且,依赖关系提供的参数不会变化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function testOne()
{
$this->assertTrue(true);
return 'Depends1';
}

/**
* @depends testOne
* @dataProvider additionProvider
*/
public function testSum()
{
$this->assertEquals(['Provider1', 'Depends1'], func_get_args());
}

//会测试两次,第一此传递:Provider1,第二次传递:Provider2
public function additionProvider()
{
return [
['Provider1'],
['Provider2'],
];
}
  • 测试结果
1
2
3
4
5
6
7
8
9
10
11
12
PHPUnit 9.5-gd3b55c36f by Sebastian Bergmann and contributors.

Failed asserting that two arrays are equal.
<Click to see difference>

/Users/caoxl/WWW/test.com/tests/MyTest.php:57

Time: 00:00.378, Memory: 8.00 MB


FAILURES!
Tests: 3, Assertions: 3, Failures: 1.

异常测试

异常测试有两种方式:

  • 在代码中使用: $this->expectException(InvalidArgumentException::class);
  • 使用标注:@expectException

断言方法/标注:

  • expectException
  • expectExceptionCode
  • expectExceptionMessage
  • expectExceptionMessageRegExp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function testException1()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage("hello");
throw new Exception('hello');
}

/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage hi
*/
public function testException2()
{
throw new InvalidArgumentException('hello');
}

注意:不允许对 :Exception 类进行测试,异常类越明确越好。

错误调试

默认情况下,在测试过程中如果触发到了 PHP 的错误/警告,PHPUnit 会将其转换为异常:

  • PHPUnit\Framework\Error\Notice
  • PHPUnit\Framework\Error\Warning
  • PHPUnit\Framework\Error\Error
1
2
3
4
5
6
public function testError()
{
$this->expectException(Error::class);
// 触发一个错误
include 'file_not_existing_file.php';
}
  • 测试结果:
1
2
3
4
5
Time: 00:00.319, Memory: 8.00 MB


FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

输出内容测试

有时候,想要断言 某方法的运行过程中生成了预期的输出(例如,通过 echoprint)。PHPUnit\Framework\TestCase 类使用 PHP 的 输出缓冲 特性来为此提供必要的功能支持。

1
2
3
4
5
6
7
8
9
10
11
public function testOutput1()
{
$this->expectOutputString("Hello");
echo "Hello";
}

public function testOutput2()
{
$this->expectOutputRegex("/\d+/");
print "Hello World";
}
  • 测试结果
1
2
3
4
5
6
7
8
// testOutput2
Failed asserting that 'Hello World' matches PCRE pattern "/\d+/".


Time: 00:00.368, Memory: 8.00 MB

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

标记未完成 与 跳过

  • 标记未完成
1
2
3
4
5
6
public function testMark()
{
$this->assertTrue(true);
// 在这里停止
$this->markTestIncomplete("后续还未完成");
}
  • 测试结果:
1
2
3
4
5
6
7
8
9
10
PHPUnit 9.5-gd3b55c36f by Sebastian Bergmann and contributors.

后续还未完成

/Users/caoxl/WWW/test.com/tests/MyTest.php:114

Time: 00:00.393, Memory: 8.00 MB

OK, but incomplete, skipped, or risky tests!
Tests: 1, Assertions: 1, Incomplete: 1.
  • 跳过测试
1
2
3
4
5
6
public function setUp(): void
{
if (!extension_loaded('mysqli')) {
$this->markTestSkipped("MySQLi 扩展不可用");
}
}
  • 测试结果:
1
2
3
4
5
6
7
8
9
10
PHPUnit 9.5-gd3b55c36f by Sebastian Bergmann and contributors.

MySQLi 扩展不可用

/Users/caoxl/WWW/test.com/tests/MyTest.php:24

Time: 00:00.425, Memory: 8.00 MB

OK, but incomplete, skipped, or risky tests!
Tests: 1, Assertions: 0, Skipped: 1.

Powered by Hexo and Hexo-theme-hiker

Copyright © 2017 - 2023 Keep It Simple And Stupid All Rights Reserved.

访客数 : | 访问量 :