第六章 装置器
编写测试最耗时的部分是边编写设置整个程序到达一个已知状态,而后在测试结束后返回到原始状态。这个已知状态叫做测试的装置器。
在例5中,装置器很简单,只是存储在变量$fixture中的数组。多数情况下,装置器会比简单数组复杂,设置代码也会相应增长。当你写几个类似的装置器时这个问题变得更糟糕。没有测试框架的帮助,我们不得不重复很多代码,为每个测试设置装置器。
PHPUnit支持共享设置代码。在测试方法运行之前,模板类方法setUp( )会被调用。setUp( )用于创建要测试的对象,当测试方法结束运行时,无论成功失败,另一个模板方法tearDown( )会被调用。tearDown( )是用于清除测试用于的对象的。
我们现在重构例5,使用setUp( )解决以前有的代码重复的问题。首先,我们定义变量$fixture的实例,用于替代局部方法中的变量。然后,我们将创建数组fixture的装置器放入方法setUp( )中。最后,我们将从测试方法中移除多余代码,使用新引进的变量实例,$this->fixture,使用assertEquals( )断言方法替代本地方法变量$fixture。
<?php
require_once 'PHPUnit2/Framework/TestCase.php';
class ArrayTest extends PHPUnit2_Framework_TestCase {
protected $fixture;
protected function setUp( ) {
// Create the Array fixture.
$this->fixture = Array( );
}
public function testNewArrayIsEmpty( ) {
// Assert that the size of the Array fixture is 0.
$this->assertEquals(0, sizeof($this->fixture));
}
public function testArrayContainsAnElement( ) {
// Add an element to the Array fixture.
$this->fixture[] = 'Element';
// Assert that the size of the Array fixture is 1.
$this->assertEquals(1, sizeof($this->fixture));
}
}
?>
每个测试运行时,各调用setUp( )和tearDown( )一次。尽管在一个测试用例类中所有的测试方法只运行一次设置和解除代码,这看起来很简单,但这么做使得书写互相完全独立的测试相当困难。
除了setUp( )和tearDown( )在每个测试中只运行一次外,每个测试在测试用例类的新实例中也只运行一次(参见本书后的PHPUnit的实现部分)。
6-1. 多用setUp( ),少用tearDown( )
理论上,setUp( )和tearDown( )是很好的对称关系,但实践中不是这样。如果你已经在setUp( )中分配了外部资源,如文件或socket,你只需要实现tearDown( )。 如果setUp() 只创造简单的PHP 对象,一般就可以忽略tearDown() 。 但是, 如果在setUp()创建了很多对象,你也许想要在tearDown()中用unset() 函数复位那些对象,这些对象会被垃圾收集器收集。测试用例对象的垃圾收集是不可预测的。
6-2. 变量
当有二个细微不同的设置过程的测试会怎么样? 有二种可能性:
如果setUp() 代码只有少许不想同,将不同的代码移出setUp(),放入测试方法。
如果setUp()完全不同,就要一个不同的测试用例类。命名不同的类名,体现在设置方法中。
6-3. 套件级设定
PHPUnit没有为套件的设定提供方便的支持。没有什么真正的原因要求在测试之间分享装置器,这种情况一般源于未解决的设计问题。
在测试间共享装置器的一个好例子是数据库连接: 登录数据库一次,然后重复利用数据库连接,而不是为每个测试创建新连接。这使测试运行得更快。要实现这种情况,将数据库测试写在名为DatabaseTests得测试用例类中,并且包装测试套件在TestSetup 装饰器对象中,重载setUp(),实现打开数据库连接,在tearDown()中关闭连接,如例6。可以通过DatabaseTestSetup 装饰器调用,通过DatabaseTests运行测试,例如,PHPUnit 的命令行测试器通过phpunit DatabaseTestSetup运行。
例6.书写套件层次的设置装饰器
<?php
require_once 'PHPUnit2/Framework/TestSuite.php';
require_once 'PHPUnit2/Extensions/TestSetup.php';
class DatabaseTestSetup extends PHPUnit2_Extensions_TestSetup
{
protected $connection = NULL;
protected function setUp( ) {
$this->connection = new PDO(
'mysql:host=wopr;dbname=test',
'root',
''
);
}
protected function tearDown( ) {
$this->connection = NULL;
}
public static function suite( ) {
return new DatabaseTestSetup(
new PHPUnit2_Framework_TestSuite('DatabaseTests')
);
}
}
?>
怎么强调都不为过,在测试间共享装置器会减少测试的价值。隐藏的设计问题是,对象捆绑的太紧密。使用残根来书写测试能够解决隐藏的设计问题,得到更好的结果(参见本书后的残根部分),这比忽视改进设计机会,创建运行时有依赖关系的测试要好的多。
--------------------------------------------------------------------------------------------------------------------
原文
Chapter 6. Fixtures
One of the most time consuming parts of writing tests is writing the code to set up the world in a known state and then return it to its original state when the test is complete. The known state is called the fixture of the test.
In Example 5, the fixture was simply an array stored in the $fixture variable. Most of the time, though, the fixture will be more complex than a simple array, and the amount of code needed to set it up will grow accordingly. The actual content of the test gets lost in the noise of setting up the fixture. This problem gets even worse when you write several tests with similar fixtures. Without some help from the testing framework, we would have to duplicate the code that sets up the fixture for each test we write.
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.
We can now refactor Example 5 and use setUp( ) to eliminate the code duplication that we had before. First, we declare the instance variable, $fixture, that we are going to use instead of a method-local variable. Then, we put the creation of the Array fixture into the setUp( ) method. Finally, we remove the redundant code from the test methods and use the newly introduced instance variable, $this->fixture, instead of the method-local variable $fixture with the assertEquals( ) assertion method.
<?php
require_once 'PHPUnit2/Framework/TestCase.php';
class ArrayTest extends PHPUnit2_Framework_TestCase {
protected $fixture;
protected function setUp( ) {
// Create the Array fixture.
$this->fixture = Array( );
}
public function testNewArrayIsEmpty( ) {
// Assert that the size of the Array fixture is 0.
$this->assertEquals(0, sizeof($this->fixture));
}
public function testArrayContainsAnElement( ) {
// Add an element to the Array fixture.
$this->fixture[] = 'Element';
// Assert that the size of the Array fixture is 1.
$this->assertEquals(1, sizeof($this->fixture));
}
}
?>
setUp( ) and tearDown( ) will be called once for each test method run. Although it might seem frugal to run the set up and tear down code only once for all the test methods in a test-case class, doing so would make it hard to write tests that are completely independent of each other.
Not only are setUp( ) and tearDown( ) run once for each test method, but the test methods are run in fresh instances of the test-case class (see "PHPUnit's Implementation," later in this book).
6-1. More setUp( ) than tearDown( )
setUp( ) and tearDown( ) are nicely symmetrical in theory but not in practice. In practice, you only need to implement tearDown( ) if you have allocated external resources such as files or sockets in setUp( ). If your setUp( ) just creates plain PHP objects, you can generally ignore tearDown( ). However, if you create many objects in your setUp( ), you might want to unset( ) the variables pointing to those objects in your tearDown( ) so they can be garbage collected. The garbage collection of test-case objects is not predictable.
6-2. Variations
What happens when you have two tests with slightly different setups? There are two possibilities:
If the setUp( ) code differs only slightly, move the code that differs from the setUp( ) code to the test method.
If you really have a different setUp( ), you need a different test-case class. Name the class after the difference in the setup.
6-3. Suite-Level Setup
PHPUnit does not provide convenient support for suite-level setup. There aren't many good reasons to share fixtures between tests, but, in most cases, the need to do so stems from an unresolved design problem.
A good example of a fixture that makes sense to share across several tests is a database connection: you log into the database once and reuse the database connection instead of creating a new connection for each test. This makes your tests run faster. To do this, write your database tests in a test-case class named DatabaseTests, and wrap the test suite in a TestSetup decorator object that overrides setUp( ) to open the database connection and tearDown( ) to close the connection, as shown in Example 6. You can run the tests from DatabaseTests tHRough the DatabaseTestSetup decorator by invoking, for instance, PHPUnit's command-line test runner with phpunit DatabaseTestSetup.
Example 6. Writing a suite-level setup decorator
<?php
require_once 'PHPUnit2/Framework/TestSuite.php';
require_once 'PHPUnit2/Extensions/TestSetup.php';
class DatabaseTestSetup extends PHPUnit2_Extensions_TestSetup
{
protected $connection = NULL;
protected function setUp( ) {
$this->connection = new PDO(
'mysql:host=wopr;dbname=test',
'root',
''
);
}
protected function tearDown( ) {
$this->connection = NULL;
}
public static function suite( ) {
return new DatabaseTestSetup(
new PHPUnit2_Framework_TestSuite('DatabaseTests')
);
}
}
?>
It cannot be emphasized enough that sharing fixtures between tests reduces the value of the tests. The underlying design problem is that objects are too closely bound together. You will achieve better results by solving the underlying design problem and then writing tests using stubs (see the section "Stubs," later in this book), than by creating dependencies between tests at runtime and ignoring the opportunity to improve your design.