第 4 章 基境(fixture)

在编写测试时,最费时的部分之一是编写代码来将整个场景设置成某个已知的状态,并在测试结束后将其复原到初始状态。这个已知的状态称为测试的 基境(fixture)

例 2.1中,基境十分简单,就是存储在 $stack 变量中的数组。然而,绝大多数时候基境均远比一个简单数组要复杂,用于建立基境的代码量也会随之增长。测试的真正内容就被淹没于建立基境带来的干扰中。当编写多个需要类似基境的测试时这个问题就变得更糟糕了。如果没有来自于测试框架的帮助,就不得不在写每一个测试时都将建立基境的代码重复一次。

PHPUnit 支持共享建立基境的代码。在运行某个测试方法前,会调用一个名叫 setUp() 的模板方法。 setUp() 是创建测试所用对象的地方。当测试方法运行结束后,不管是成功还是失败,都会调用另外一个名叫 tearDown() 的模板方法。tearDown() 是清理测试所用对象的地方。

例 2.2中,我们在测试之间运用生产者-消费者关系来共享基境。通常这并非所预期的,甚至是不可能的。例 4.1展示了另外一个编写测试 StackTest 的方式。在这个方式中,不再重用基境本身,而是重用建立基境的代码。首先声明一个实例变量,$stack,用来替代方法内的局部变量。然后把 array 基境的建立放到 setUp() 方法中。最后,从测试方法中去除冗余代码,在 assertEquals() 断言方法中使用新引入的实例变量,$this->stack,替代方法内的局部变量 $stack

例 4.1: 用 setUp() 建立栈的基境

<?php
class StackTest extends PHPUnit_Framework_TestCase
{
    protected $stack;

    protected function setUp()
    {
        $this->stack = array();
    }

    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));
    }
}
?>


测试类的每个测试方法都会运行一次 setUp()tearDown() 模板方法(同时,每个测试方法都是在一个全新的测试类实例上运行的)。

另外,setUpBeforeClass()tearDownAfterClass() 模板方法将分别在测试用例类的第一个测试运行之前和测试用例类的最后一个测试运行之后调用。

下面这个例子中展示了测试用例类中所有有效的模板方法。

例 4.2: 展示所有有效模板方法的例子

<?php
class TemplateMethodsTest extends PHPUnit_Framework_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 4.2.0 by Sebastian Bergmann.

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() 是精确对称的,但是实践中并非如此。实际上,只有在 setUp() 中分配了诸如文件或套接字之类的外部资源时才需要实现 tearDown() 。如果 setUp() 中只创建纯 PHP 对象,通常可以略过 tearDown()。不管怎样,如果在 setUp() 中创建了大量对象,你可能想要在 tearDown()unset() 指向这些对象的变量,这样它们就可以被垃圾回收机制回收掉。对测试用例对象的垃圾回收动作则是不可预知的。

变体

如果两个基境建立工作略有不同的测试该怎么办? 出现这种情况有两个可能性:

  • 如果两个 setUp() 代码仅有微小差异,把有差异的代码内容从 setUp() 移到测试方法内。

  • 如果两个 setUp() 是确实不一样,那么需要另外一个测试用例类。参考基境建立工作的不同之处来命名这个类。

共享基境

有少数几个很好的理由来在测试之间共享基境,但是大部分情况下,在测试之间共享基境的需求都是源于某个未解决的设计问题。

一个有实际意义的多测试间共享基境的例子是数据库连接:只登录数据库一次,然后重用此连接,而不是每个测试都建立一个新的数据库连接。这样能加快测试的运行。

例 4.3setUpBeforeClass()tearDownAfterClass() 模板方法来分别在测试用例类的第一个测试之前和最后一个测试之后连接与断开数据库。

例 4.3: 在同一个测试套件内的不同测试之间共享基境

<?php
class DatabaseTest extends PHPUnit_Framework_TestCase
{
    protected static $dbh;

    public static function setUpBeforeClass()
    {
        self::$dbh = new PDO('sqlite::memory:');
    }

    public static function tearDownAfterClass()
    {
        self::$dbh = NULL;
    }
}
?>


需要反复强调的是:在测试之间共享基境会降低测试的价值。潜在的设计问题是对象之间不是松散耦合的。如果解决掉潜在的设计问题,并使用短连件(stub)(参见 第 9 章)来编写测试,就能达成更好的结果,而不是在测试之间建立运行时依赖并忽略改进设计的机会。

全局状态

使用单件(singleton)的代码很难测试。使用全局变量的代码也一样。通常情况下,要测试的代码和全局变量之间强烈耦合,并且无法控制它的创建。另外一个问题是,一个测试对全局变量的改变可能会破坏另外一个测试。

在 PHP 中,全局变量是这样运作的:

  • 全局变量 $foo = 'bar'; 是存储为 $GLOBALS['foo'] = 'bar'; 的。

  • $GLOBALS 变量称为超全局变量。

  • 超全局变量是内建变量,在任何变量作用域中都是可用的。

  • 在函数或者方法的变量作用域中,要访问全局变量 $foo,可以直接访问 $GLOBALS['foo'],或者用 global $foo; 来创建一个引用全局变量的局部变量。

除了全局变量,类的静态属性也是一种全局状态。

默认情况下,PHPUnit 用一种对全局变量与超全局变量($GLOBALS, $_ENV, $_POST, $_GET, $_COOKIE, $_SERVER, $_FILES, $_REQUEST)进行更改不会影响到其他测试的方式来运行所有测试。还可以选择将这种隔离扩展到类的静态属性。

注意

对类的静态属性的备份与还原操作其实现方案需要 PHP 5.3(或更高版本).

对全局变量和类的静态属性的备份与还原操作其实现方案使用了 serialize()unserialize()

某些 PHP 自身提供的类,比如 PDO ,其实例对象无法序列化,因此如果把这样一个对象存放在比如说 $GLOBALS 数组内时,备份操作就会出问题。

“@backupGlobals”一节中所讨论的 @backupGlobals 标注可以用来控制对全局变量的备份与还原操作。另外,还可以提供一个全局变量的黑名单,黑名单中的全局变量将被排除于备份与还原操作之外,就像这样:

class MyTest extends PHPUnit_Framework_TestCase
{
    protected $backupGlobalsBlacklist = array('globalVariable');

    // ...
}

注意

请注意,在方法内(例如在 setUp() 内)对 $backupGlobalsBlacklist 属性进行设置是无效的。

“@backupStaticAttributes”一节中所讨论的 @backupStaticAttributes 标注可以用来控制对静态属性的备份与还原操作。另外,还可以提供一个静态属性的黑名单,黑名单中的静态属性将被排除于备份与还原操作之外,就像这样:

class MyTest extends PHPUnit_Framework_TestCase
{
    protected $backupStaticAttributesBlacklist = array(
      'className' => array('attributeName')
    );

    // ...
}

注意

请注意,在方法内(例如在 setUp() 内)对 $backupStaticAttributesBlacklist 属性进行设置是无效的。