Fixtures
Fixtures
编写测试中最耗时的部分之一是编写代码以将世界置于已知状态,然后在测试完成时将其返回到其原始状态。这个已知的状态被称为测试的夹具
。
在例2.1中,夹具只是存储在$stack
变量中的数组。然而,大多数时候,夹具将比简单的数组更复杂,并且设置它所需的代码量也会相应增加。测试的实际内容会因设置固定装置的噪音而丢失。当你用类似的灯具写几个测试时,这个问题会变得更糟。如果没有来自测试框架的帮助,我们将不得不复制为我们编写的每个测试设置夹具的代码。
PHPUnit supports sharing the setup code. Before a test method is run, a template method called `setUp()` is invoked. `setUp()` is where you create the objects against which you will test. Once the test method has finished running, whether it succeeded or failed, another template method called `tearDown()` is invoked. `tearDown()` is where you clean up the objects against which you tested.
在例2.2中,我们使用测试之间的生产者 - 消费者关系共享一个夹具。这并不总是需要或甚至可能的。例4.1展示了我们如何编写测试,StackTest以便不重复使用灯具本身,而是创建它的代码。首先我们声明实例变量,$stack我们将使用它来代替方法局部变量。然后,我们将array夹具的创建放入setUp()方法中。最后,我们从测试方法中删除冗余代码,并使用新引入的实例变量$this->stack,而不是方法局部变量$stack和assertEquals()断言方法。
例4.1:使用 setUp()创建堆栈灯具
<?php
use PHPUnit\Framework\TestCase;
class StackTest extends TestCase
{
protected $stack;
protected function setUp()
{
$this->stack = [];
}
public function testEmpty()
{
$this->assertTrue(empty($this->stack)
}
public function testPush()
{
array_push($this->stack, 'foo'
$this->assertEquals('foo', $this->stack[count($this->stack)-1]
$this->assertFalse(empty($this->stack)
}
public function testPop()
{
array_push($this->stack, 'foo'
$this->assertEquals('foo', array_pop($this->stack)
$this->assertTrue(empty($this->stack)
}
}
?>
The `setUp()` and `tearDown()` template methods are run once for each test method (and on fresh instances) of the test case class.
In addition, the `setUpBeforeClass()` and `tearDownAfterClass()` template methods are called before the first test of the test case class is run and after the last test of the test case class is run, respectively.
以下示例显示了测试用例类中可用的所有模板方法。
例4.2:显示所有可用模板方法的例子
<?php
use PHPUnit\Framework\TestCase;
class TemplateMethodsTest extends TestCase
{
public static function setUpBeforeClass()
{
fwrite(STDOUT, __METHOD__ . "\n"
}
protected function setUp()
{
fwrite(STDOUT, __METHOD__ . "\n"
}
protected function assertPreConditions()
{
fwrite(STDOUT, __METHOD__ . "\n"
}
public function testOne()
{
fwrite(STDOUT, __METHOD__ . "\n"
$this->assertTrue(true
}
public function testTwo()
{
fwrite(STDOUT, __METHOD__ . "\n"
$this->assertTrue(false
}
protected function assertPostConditions()
{
fwrite(STDOUT, __METHOD__ . "\n"
}
protected function tearDown()
{
fwrite(STDOUT, __METHOD__ . "\n"
}
public static function tearDownAfterClass()
{
fwrite(STDOUT, __METHOD__ . "\n"
}
protected function onNotSuccessfulTest(Exception $e)
{
fwrite(STDOUT, __METHOD__ . "\n"
throw $e;
}
}
?>
phpunit TemplateMethodsTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.
TemplateMethodsTest::setUpBeforeClass
TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testOne
TemplateMethodsTest::assertPostConditions
TemplateMethodsTest::tearDown
.TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testTwo
TemplateMethodsTest::tearDown
TemplateMethodsTest::onNotSuccessfulTest
FTemplateMethodsTest::tearDownAfterClass
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) TemplateMethodsTest::testTwo
Failed asserting that <boolean:false> is true.
/home/sb/TemplateMethodsTest.php:30
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
更多的 setUp() 比 tearDown()
setUp()
而tearDown()
在理论上是很好的对称但事实上并非如此。在实践中,tearDown()
如果你已经分配了诸如文件或套接字之类的外部资源,你只需要实现setUp()
。如果你setUp()
只是创建普通的 PHP 对象,你通常可以忽略tearDown()
。但是,如果您在自己中创建了多个对象setUp()
,则可能需要unset()
指向这些对象的变量,tearDown()
以便它们可以被垃圾收集。测试用例对象的垃圾回收是不可预测的。
Variations
当你有两个稍微不同的设置测试会发生什么?有两种可能性:
- 如果
setUp()
代码略有不同,请将与代码不同的setUp()
代码移至测试方法。
- 如果你真的有不同
setUp()
,你需要一个不同的测试用例类。在设置差异后命名该类。
共享夹具
测试之间共享夹具的原因很少,但在大多数情况下,在测试之间共享夹具需要源于未解决的设计问题。
一个可以在多个测试中共享的夹具的好例子是数据库连接:您只需登录一次数据库并重新使用数据库连接,而不是为每个测试创建一个新连接。这使您的测试运行得更快。
例4.3使用setUpBeforeClass()
和tearDownAfterClass()
模板方法分别在测试用例类的第一次测试之前连接到数据库,并在测试用例的最后一次测试之后断开与数据库的连接。
例4.3:在测试套件的测试之间共享夹具
<?php
use PHPUnit\Framework\TestCase;
class DatabaseTest extends TestCase
{
protected static $dbh;
public static function setUpBeforeClass()
{
self::$dbh = new PDO('sqlite::memory:'
}
public static function tearDownAfterClass()
{
self::$dbh = null;
}
}
?>
在测试之间共享固定装置会降低测试的价值,这是不够强调的。潜在的设计问题是对象不是松散耦合的。除了在运行时创建测试之间的依赖关系并且忽略改进设计的机会之外,您将获得解决底层设计问题的更好结果,然后使用存根编写测试(请参阅第9章)。
全局状态
很难测试使用单例的代码。使用全局变量的代码也是如此。通常,您要测试的代码与全局变量强烈耦合,并且无法控制其创建。另一个问题是,一个测试对全局变量的更改可能会打破另一个测试。
在 PHP 中,全局变量的工作方式如下所示:
- 全局变量
$foo = 'bar';
存储为$GLOBALS['foo'] = 'bar';
。
- 该
$GLOBALS
变量是一个所谓的超级全局
变量。
- 超级全局变量是内置的变量,在所有范围内始终可用。
- 在函数或方法的范围内,您可以
$foo
通过直接访问$GLOBALS['foo']
或使用global $foo;
通过引用全局变量创建局部变量来访问全局变量。
除了全局变量之外,类的静态属性也是全局状态的一部分。
此前6版本,默认情况下,PHPUnit 的跑的方式,其中对全局和超全局变量(你的测试$GLOBALS
,$_ENV
,$_POST
,$_GET
,$_COOKIE
,$_SERVER
,$_FILES
,$_REQUEST
)不影响其他测试。
从版本6开始,PHPUnit 不再默认对全局变量和超全局变量执行此备份和恢复操作。可以使用 XML 配置文件中的--globals-backup
选项或设置来激活它backupGlobals="true"
。
通过使用 XML 配置文件中的--static-backup
选项或设置backupStaticAttributes="true"
,可以将此隔离扩展为类的静态属性。
全局变量和静态类属性的备份和恢复操作使用serialize()
和unserialize()
。
某些类(例如,PDO
)的对象不能被序列化,并且当这样的对象被存储在例如$GLOBALS
阵列中时,备份操作将会中断。
@backupGlobals
可以使用 “@backupGlobals
” 一节中讨论 的注释来控制全局变量的备份和恢复操作。或者,您可以提供一个全局变量的黑名单,这些备份和恢复操作不需要这样做
class MyTest extends TestCase
{
protected $backupGlobalsBlacklist = ['globalVariable'];
// ...
}
$backupGlobalsBlacklist
在例如setUp()
方法中设置属性不起作用。
@backupStaticAttributes
在 “@backupStaticAttributes
” 部分中讨论的注释可用于在每次测试之前备份所有声明类中的所有静态属性值,并在之后进行恢复。
它处理在测试开始时声明的所有类,而不仅仅是测试类本身。它只适用于静态类属性,而不适用于函数内的静态变量。
该@backupStaticAttributes
操作在测试方法之前执行,但仅在启用时才执行。如果一个静态值被之前执行的未@backupStaticAttributes
启用的测试所改变,那么该值将被备份和恢复 - 而不是最初声明的默认值。PHP 不记录最初声明的任何静态变量的默认值。
这同样适用于在测试中新加载/声明的类的静态属性。在测试之后,它们不能被重置为其最初声明的默认值,因为该值是未知的。无论设置哪个值都会泄漏到后续测试中。
对于单元测试,建议您在setUp()
代码中明确重置测试中的静态属性值(并且理想情况下也是tearDown()
这样,以免影响后续执行的测试)。
您可以提供要从备份和恢复操作中排除的静态属性的黑名单:
class MyTest extends TestCase
{
protected $backupStaticAttributesBlacklist = [
'className' => ['attributeName']
];
// ...
}
$backupStaticAttributesBlacklist
在例如setUp()
方法中设置属性不起作用。