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

时间:2016-01-30 02:37:33

标签: php unit-testing phpunit hierarchical

我已经看到Hierarchit Context Runner在JUnit中是如何工作的,它非常棒。

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

在PHPUnit中有这样的东西会很高兴,但我无法实现这一点。

我尝试在自定义方法上使用@before注释,希望能够规定顺序。此外,我试图声明内部类,但后来发现在PHP 5中不允许这样做。我还尝试了很多其他的东西但没有成功。

是否可以使用PHPUnit实现此目的?

2 个答案:

答案 0 :(得分:8)

您无法完全完成 JUnit Heirarchical Context Runner所做的事情,因为正如您所发现的那样,您无法在PHP中嵌套类。 Heirarchical Context Runner依赖于嵌套类。但是,你可以非常接近同样的事情。最后,通过考虑如何命名测试方法,您可以生成更易于导航和理解的更清晰的代码,与使用嵌套类相比,意外引入全局状态或隐藏依赖项的风险更小。

重要警告

在我们潜入之前,请注意you generally do not want to share fixtures or other state between tests。单元测试的重点是测试单个代码单元,当您通过跨测试持久(或更差,实际可变)数据创建这些单元之间的链接时,这很难做到。作为explained in the PHPUnit docs

  

在测试之间分享灯具的理由很少,但在大多数情况下需要在测试之间共享灯具源于未解决的设计问题。 [强调补充]

     

在多个测试中共享有意义的夹具的一个很好的例子是数据库连接:您登录数据库一​​次并重用数据库连接,而不是为每个测试创建新连接。这使您的测试运行得更快。

使用引导程序文件

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

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

phpunit --bootstrap src/autoload.php tests

您还可以在XML配置文件中指定引导程序文件,如下所示:

<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。在这里,您可以将所有应该在任何测试之前运行的代码放在该类中。但是,它只运行一次,因此您无法在测试之间运行

例如,您可以在运行任何测试之前为一个或多个方案填充数据:

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

(不要注意这个样本中的实际逻辑;这里的测试是蹩脚的,实际上并没有测试一个类。重点是设置。)

考虑在测试类中使用每种目标方法的多种测试方法

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

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

这样做的好处是可以在一个地方合并一个类的所有测试,如果你聪明地命名它们,即使使用大量的测试方法也可以轻松导航。

考虑使用数据提供者方法

您还可以使用data provider method。来自the manual

  

测试方法可以接受任意参数。这些参数将由数据提供程序方法(Example 2.5中的provider())提供。要使用的数据提供程序方法是使用@dataProvider注释指定的。

     

有关详细信息,请参阅the section called “Data Providers”

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

考虑使用依赖关系

您还可以强制测试类中​​的测试按specifying dependencies between them的特定顺序运行。您可以使用docblock中的@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的结果作为参数,如果其中一个测试失败,则根本不会运行。

考虑为每个目标类使用多个测试类

如果要在非常不同场景上运行大量测试,可以考虑为给定目标类创建多个测试类。然而,这通常不是你最好的选择。这意味着创建和维护更多的类(因此,如果你只在一个文件中放置一个类,就像你应该的那样,更多的文件),这使得一次看到和理解所有测试代码变得更加困难。这仅适用于从一个测试到另一个测试的非常不同方式的目标类的实例。

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

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

您还可以告诉PHPUnit运行"test suite."这允许您以逻辑方式对测试进行分组(例如,所有数据库测试或具有i18n逻辑的类的所有测试)。使用XML配置文件编写测试套件时,可以明确告诉PHPUnit以特定顺序运行测试。正如in the docs所述,

  

如果当前工作目录中存在phpunit.xmlphpunit.xml.dist(按此顺序)并且未使用--configuration,则将自动从该文件中读取配置。

     

执行测试的顺序可以明确:

     

示例5.2:使用XML配置编写测试套件

   <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>

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

全部放在一起

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

还有很多其他方法可以强制测试按特定顺序运行,我已在上面列出。但它们都引入了某种形式的全球状态,混乱或两者兼而有之。每当您进行一次测试时,只有在另一次测试完成后才会运行,您可能会在没有意识到的情如果您的测试相互依赖,那么您不会真正单元测试。在某种程度上,您正在进行集成测试。

通常,您最好进行真正的单元测试。测试目标类的每个公共方法,就好像没有其他任何存在一样。如果你能做到这一点并让你的测试通过所有可能的场景,那么你有可靠的代码。

答案 1 :(得分:0)

最后,我找到了一种方法来实现与JUnit的Hierarchical Runner获得的几乎相同的行为。

关键是使用@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上的字段外,你的测试还有参数,你必须在provider方法中返回一个数组数组。

使用DataProviders有很多好处,你可以在中找到它们 documentation