是否有用于 PHPUnit 的分层测试运行器?

Is there a Hierarchical Test Runner for PHPUnit?

我已经了解了 Hierarchical Context Runner 在 JUnit 中的工作原理,它非常棒。

它允许您在单个测试中的方法组之前安排多个设置 class。当您测试多个场景时,这非常有用;感觉更像是做 BDD。 Hierarchical Runner Explanation

如果在 PHPUnit 中有这样的东西就好了,但我就是做不到。

我试过在自定义方法上使用 @before 注释,希望能规定顺序。另外,我试图声明内部 classes,但后来我发现 PHP 5 中不允许这样做。我也尝试了许多其他事情但没有成功。

是否可以使用 PHPUnit 来实现?

您不能完全 JUnit 层次上下文运行 所做的事情,因为正如您所发现的,您不能嵌套class es 在 PHP 中。层次上下文 运行ner 依赖于嵌套的 classes。但是,您可以非常接近同一件事。最终,考虑到如何命名测试方法,您可以生成更清晰的代码,易于导航和理解,与使用嵌套 classes 相比,意外引入全局状态或隐藏依赖项的风险更小。

重要警告

在我们开始之前,请注意 you generally do not want to share fixtures or other state between tests. The whole point of unit testing is to test individual units of code, which is hard to do when you also create links between those units by having persistent (or worse, actually variable) data across tests. As explained in the PHPUnit docs,

There are few good reasons to share fixtures between tests, but in most cases the need to share a fixture between tests stems from an unresolved design problem. [emphasis added]

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.

使用 Bootstrap 文件

如果您的代码应该在 所有 测试之前 运行 一次,那么在所有测试中 classes,use a bootstrap file .例如,如果您的代码依赖于自动加载器或常量,例如包含特定文件的路径,或者如果您只需要 运行 一系列 includerequire 语句来加载一些函数,使用 bootstrap 文件。

您可以使用 --bootstrap 命令行选项执行此操作,如下所示:

phpunit --bootstrap src/autoload.php tests

您还可以在 XML 配置文件中指定一个 bootstrap 文件,如下所示:

<phpunit bootstrap="src/autoload.php">
  <!-- other configuration stuff here -->
</phpunit>

使用设置方法

你也可以specify a setup method to run before running any other tests in a particular class。这是在 class 中的 any 测试之前放置所有应该 运行 的代码的地方。但是,它只会 运行 一次,因此您不能将它用于 运行 测试之间。

例如,您可以执行此操作以在 运行任何测试之前为一个或多个场景填充数据:

<?php
class NameValidatorTest extends PHPUnit_Framework_TestCase
{
    protected static $nameForEnglishScenario;
    protected static $nameForFrenchScenario;

    /**
     * Runs before any tests and sets up data for the test
     */
    public static function setUpBeforeClass()
    {
        self::$nameForEnglishScenario = 'John Smith';
        self::$nameForFrenchScenario = 'Séverin Lemaître';
    }

    public function testEnglishName()
    {
        $this->assertRegExp('/^[a-z -]+$/i', self::$nameForEnglishScenario);
    }

    public function testFrenchName()
    {
        $this->assertRegExp('/^[a-zàâçéèêëîïôûùüÿñæœ -]+$/i', self::$nameForFrenchScenario);
    }
}

(不要注意这个示例中的实际逻辑;这里的测试很蹩脚,实际上并没有测试 class。重点在于设置。)

考虑在测试中对每个目标方法使用多种测试方法Class

测试多个场景的典型方法是创建多个方法,其名称反映它们的条件。例如,如果我正在测试名称验证器 class,我可能会这样做:

<?php
class NameValidatorTest extends PHPUnit_Framework_TestCase
{
    public function testIsValid_Invalid_English_Actually_French()
    {
        $validator = new NameValidator();
        $validator->setName('Séverin Lemaître');
        $validator->setLocale('en');
        $this->assertFalse($validator->isValid());
    }

    public function testIsValid_Invalid_French_Gibberish()
    {
        $validator = new NameValidator();
        $validator->setName('Séverin J*E08RW)8WER Lemaître');
        $validator->setLocale('fr');
        $this->assertFalse($validator->isValid());
    }

    public function testIsValid_Valid_English()
    {
        $validator = new NameValidator();
        $validator->setName('John Smith');
        $validator->setLocale('en');
        $this->assertTrue($validator->isValid());
    }

    public function testIsValid_Valid_French()
    {
        $validator = new NameValidator();
        $validator->setName('Séverin Lemaître');
        $validator->setLocale('fr');
        $this->assertTrue($validator->isValid());
    }
}

这样做的好处是可以将 class 的所有测试整合到一个地方,如果您巧妙地命名它们,即使有很多测试方法也可以轻松导航。

考虑使用数据提供者方法

你也可以使用 data provider method. From the manual:

A test method can accept arbitrary arguments. These arguments are to be provided by a data provider method (provider() in Example 2.5). The data provider method to be used is specified using the @dataProvider annotation.

See the section called “Data Providers” for more details.

您可以使用数据提供程序多次 运行 相同的测试代码,为每个 运行 测试不同的场景使用不同的数据。

考虑使用依赖项

您还可以按 specifying dependencies between them 的特定顺序强制测试 class 到 运行 中的测试。您可以在文档块中使用 @depends 来执行此操作。文档中的示例:

<?php
class MultipleDependenciesTest extends PHPUnit_Framework_TestCase
{
    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     */
    public function testConsumer()
    {
        $this->assertEquals(
            array('first', 'second'),
            func_get_args()
        );
    }
}

在这个例子中,testProducerFirsttestProducerSecond都保证在testConsumer之前运行。但是请注意,testConsumer 将接收 testProducerFirsttestProducerSecond 的结果作为参数,如果其中一个测试失败,它根本不会 运行。

考虑对每个目标使用多重测试 Classes Class

如果您想在非常 不同的场景中运行 进行大量测试,您可以考虑为给定目标创建多个测试class class。然而,这通常不是您最好的选择。这意味着创建和维护更多的 classes(因此更多的文件,如果你只将一个 class 放在一个文件中,正如你应该做的那样),并且它使得查看和理解所有你的测试代码一次。这仅适用于您的目标 class 用于 非常 不同测试方式的情况。

但是,如果您正在编写 SOLID 代码并正确使用设计模式,那么您的代码不应该能够 运行 在如此不同的条件下运行,这才有意义。所以,这有点见仁见智,但这可能永远不是编写测试的正确方法。

考虑使用测试套件运行 以特定顺序进行测试

你也可以告诉PHP单元到运行一个"test suite." This allows you to group tests in a logical manner (e.g., all database tests or all tests for classes with i18n logic). When you compose your test suite using an XML configuration file, you can tell PHPUnit explicitly to run your tests in a particular order. As explained in the docs,

If phpunit.xml or phpunit.xml.dist (in that order) exist in the current working directory and --configuration is not used, the configuration will be automatically read from that file.

The order in which tests are executed can be made explicit:

Example 5.2: Composing a Test Suite Using XML Configuration

   <phpunit bootstrap="src/autoload.php">
     <testsuites>
       <testsuite name="money">
         <file>tests/IntlFormatterTest.php</file>
         <file>tests/MoneyTest.php</file>
         <file>tests/CurrencyTest.php</file>
       </testsuite>
     </testsuites>
   </phpunit>

这样做的缺点是,您再次引入了一种全局状态形式。如果在您的实际应用程序中,您在 运行 从 IntlFormatter class 中使用一些关键功能之前使用 Money class 怎么办?

把它们放在一起

最好的办法是使用 setUpBeforeClass() 方法在每个测试 class 的基础上进行设置。然后,每个目标方法使用多个测试方法来测试您的各种场景。

还有许多其他方法可以按照我在上面列出的特定顺序强制测试 运行。但它们都引入了某种形式的全局状态、混乱或两者兼而有之。任何时候你只进行一个测试 运行 之后另一个测试完成,你就有在没有意识到的情况下引入依赖性的风险。如果您的测试相互依赖,您就不是 真正 单元测试。在某种程度上,您是在进行集成测试。

通常,您最好进行真正的 unit 测试。测试目标 class 的每个 public 方法,就好像没有其他方法存在一样。当您可以做到这一点并使您的测试通过所有可能的场景时,然后您就有了可靠的代码。

最后,我找到了一种方法来实现与使用 JUnit 的分层运行程序几乎相同的行为。

关键是使用了 @dataProvider 注释。对于需要 特定设置 的每个测试子组,您可以创建一个包含此设置逻辑的新方法,并指定 dataProvider测试。

  /**
   * @dataProvider provider
   */
  public function testWithDataProvider($arg1, $arg2) {
    //test logic
    //assertion
  }

  public function provider() {
    //custom setup before running "testWithDataProvider" 
    return array(
      array(0, 1),
      array(1,2)
    );
  }

逻辑有点不同,现在你的测试有参数,除了你的 testClass 上的字段,你必须 return 你的提供者方法中的数组数组。

使用 DataProvider 有更多好处,您可以在 documentation