按特定顺序运行 PHPUnit 测试

Posted

技术标签:

【中文标题】按特定顺序运行 PHPUnit 测试【英文标题】:Run PHPUnit Tests in Certain Order 【发布时间】:2010-09-05 19:56:11 【问题描述】:

有没有办法让TestCase 中的测试按特定顺序运行?例如,我想将对象的生命周期从创建到使用到销毁分开,但我需要确保在运行其他测试之前先设置对象。

【问题讨论】:

您可以按照下面的答案中所述添加@depends,并且使用 setup() 和 teardown() 也是一个好主意,但测试只是从上到下运行...... 另外一个似乎没有被覆盖的用例:也许所有的测试都是原子的,但有些测试很慢。我希望尽快运行快速测试,以便它们可以快速失败,并且在我已经看到其他问题并且可以立即解决它们之后,任何慢速测试最后运行。 【参考方案1】:

phpUnit 通过@depends 注解支持测试依赖。

这是文档中的一个示例,其中测试将以满足依赖关系的顺序运行,每个依赖测试将参数传递给下一个:

class StackTest extends PHPUnit_Framework_TestCase

    public function testEmpty()
    
        $stack = array();
        $this->assertEmpty($stack);

        return $stack;
    

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack)
    
        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    

    /**
     * @depends testPush
     */
    public function testPop(array $stack)
    
        $this->assertEquals('foo', array_pop($stack));
        $this->assertEmpty($stack);
    

但是,重要的是要注意,具有未解决依赖关系的测试将被执行(这是理想的,因为这会很快引起对失败测试的注意)。因此,在使用依赖项时要密切注意。

【讨论】:

对于 PHPUnit,这意味着如果之前的测试没有执行,测试函数将被跳过。这不会创建测试订单。 只是为了扩展@Dereckson,如果依赖于 either 的测试尚未运行或失败,@depends 注释将导致跳过测试当它运行时。 @km6zla 这是否意味着当我们在文件中将testPop() 方法放在或写入testPush() 之前,那么testPop() 将永远不会被执行并总是被跳过?【参考方案2】:

您的测试中可能存在设计问题。

通常每个测试都不能依赖于任何其他测试,因此它们可以按任何顺序运行。

每个测试都需要实例化和销毁它运行所需的一切,这将是完美的方法,您永远不应该在测试之间共享对象和状态。

您能否更具体地说明为什么 N 次测试需要同一个对象?

【讨论】:

这对我来说似乎不正确。单元测试的重点是测试整个单元。拥有一个单元的目的是将必须相互依赖的事物组合在一起。在没有类上下文的情况下编写测试单个方法的测试类似于提倡过程式编程而不是 oo,因为您提倡单个函数不应依赖于相同的数据。 我不同意你的观点。实例化测试的输出是一个有效的对象,可供测试套件中的其他测试使用。不需要为每个测试实例化一个新对象,特别是在构造函数很复杂的情况下。 如果构造函数很复杂,你做错了什么,可能你的类做的太多了。请阅读“SOLID”,更详细地了解“单一责任模式(SRP)”,您还应该使用模拟“伪造”测试中的依赖关系,也请阅读“模拟、​​伪造和存根”。 @emfi 如果你正在测试一个真实的数据库,你没有在做 unit 测试。您正在进行功能测试。要进行单元测试,您必须模拟 DB 适配器,并且正如 Fabio 所说,您需要在每次测试运行时实例化您的 SUT(被测系统)。如果你要为每个测试重复一些东西,你可以使用受保护的setUp() 方法来准备模拟。 @emfi 如果您正在测试的代码实际上是通往数据库的桥梁(例如,ORM、ODM 等)(例如,您是Doctrine 项目或您的公司正在开发 Doctrine 的替代品)您可能正在模拟连接对象并测试如何使用连接以查看创建了哪些查询等。唯一可以进入真实数据库的测试可以作为 Connection 对象本身的测试,它将通过有限的测试访问一个非常受控的真实连接。【参考方案3】:

正确的答案是正确的测试配置文件。我遇到了同样的问题,并通过创建具有必要测试文件顺序的测试套件来解决它:

phpunit.xml:

<phpunit
        colors="true"
        bootstrap="./tests/bootstrap.php"
        convertErrorsToExceptions="true"
        convertNoticesToExceptions="true"
        convertWarningsToExceptions="true"
        strict="true"
        stopOnError="false"
        stopOnFailure="false"
        stopOnIncomplete="false"
        stopOnSkipped="false"
        stopOnRisky="false"
>
    <testsuites>
        <testsuite name="Your tests">
            <file>file1</file> //this will be run before file2
            <file>file2</file> //this depends on file1
        </testsuite>
    </testsuites>
</phpunit>

【讨论】:

我认为这是唯一可靠的解决方案 完美!并非每个测试都是单元测试;例如,在编写 HTTP 请求或功能测试时,可能需要跨测试类保留状态更改,在这种情况下,这是以有意义的顺序运行测试的最可靠方法。 有人测试过,如果这对于 PHPUnit 测试的并行执行是正确的? 这个答案是否暗示每个测试文件都需要明确列出,即使有数百个测试文件?这看起来不是一个好的解决方案。 @AttilaSzeremi 不幸的是,是的。从那以后我确实研究了这个问题,所以现在也许有一个更好的问题。我相信有一个可行的(虽然不是完美的)解决方案总比没有解决方案要好:)【参考方案4】:

如果您希望您的测试共享各种帮助对象和设置,您可以使用setUp()tearDown() 添加到sharedFixture 属性。

【讨论】:

你还能assertEquals()setUp()吗?这是不好的做法吗?【参考方案5】:

PHPUnit 允许使用“@depends”注解来指定依赖测试用例并允许在依赖测试用例之间传递参数。

【讨论】:

【参考方案6】:

替代解决方案: 在测试中使用 static(!) 函数来创建可重用的元素。例如(我使用 selenium IDE 记录测试和 phpunit-selenium (github) 在浏览器中运行测试)

class LoginTest extends SeleniumClearTestCase

    public function testAdminLogin()
    
        self::adminLogin($this);
    

    public function testLogout()
    
        self::adminLogin($this);
        self::logout($this);
    

    public static function adminLogin($t)
    
        self::login($t, 'john.smith@gmail.com', 'pAs$w0rd');
        $t->assertEquals('John Smith', $t->getText('css=span.hidden-xs'));
    

    // @source LoginTest.se
    public static function login($t, $login, $pass)
    
        $t->open('/');
        $t->click("xpath=(//a[contains(text(),'Log In')])[2]");
        $t->waitForPageToLoad('30000');
        $t->type('name=email', $login);
        $t->type('name=password', $pass);
        $t->click("//button[@type='submit']");
        $t->waitForPageToLoad('30000');
    

    // @source LogoutTest.se
    public static function logout($t)
    
        $t->click('css=span.hidden-xs');
        $t->click('link=Logout');
        $t->waitForPageToLoad('30000');
        $t->assertEquals('PANEL', $t->getText("xpath=(//a[contains(text(),'Panel')])[2]"));
    

好的,现在,我可以在其他测试中使用这个可重用的元素了 :) 例如:

class ChangeBlogTitleTest extends SeleniumClearTestCase

    public function testAddBlogTitle()
    
      self::addBlogTitle($this,'I like my boobies');
      self::cleanAddBlogTitle();
    

    public static function addBlogTitle($t,$title) 
      LoginTest::adminLogin($t);

      $t->click('link=ChangeTitle');
      ...
      $t->type('name=blog-title', $title);
      LoginTest::logout($t);
      LoginTest::login($t, 'paris@gmail.com','hilton');
      $t->screenshot(); // take some photos :)
      $t->assertEquals($title, $t->getText('...'));
    

    public static function cleanAddBlogTitle() 
        $lastTitle = BlogTitlesHistory::orderBy('id')->first();
        $lastTitle->delete();
    
通过这种方式,您可以构建测试的层次结构。 您可以保持每个测试用例完全独立的属性(如果您在每次测试后清理数据库)。 最重要的是,例如,如果将来登录方式发生变化,您只需修改 LoginTest 类,其他测试中不需要正确的登录部分(更新 LoginTest 后它们应该可以工作):)

当我运行测试时,我的脚本开始清理数据库广告。上面我使用了我的SeleniumClearTestCase 类(我在那里制作 screenshot() 和其他不错的功能)它是MigrationToSelenium2 的扩展(从 gi​​thub,使用 seleniumIDE + ff 插件“Selenium IDE:PHP Formatters”将记录的测试移植到 Firefox 中)这是我的类 LaravelTestCase 的扩展(它是 Illuminate\Foundation\Testing\TestCase 的副本,但不扩展 PHPUnit_Framework_TestCase),它设置 laravel 以在我们想要在测试结束时清理数据库时访问 eloquent)这是 PHPUnit_Extensions_Selenium2TestCase 的扩展。为了设置 laravel eloquent,我还在 SeleniumClearTestCase 函数 createApplication 中(在 setUp 调用,我从 laral test/TestCase 中获取此函数)

【讨论】:

这里是在 Laravel 5.2 和 phpUnit 上的 Selenium IDE 中运行测试的更多详细信息:***.com/questions/33845828/…【参考方案7】:

在我看来,以下面的场景为例,我需要测试特定资源的创建和销毁。

最初我有两种方法,a. testCreateResource 和 b. testDestroyResource

一个。测试创建资源

<?php
$app->createResource('resource');
$this->assertTrue($app->hasResource('resource'));
?>

b. testDestroyResource

<?php
$app->destroyResource('resource');
$this->assertFalse($app->hasResource('resource'));
?>

我认为这是一个坏主意,因为 testDestroyResource 依赖于 testCreateResource。更好的做法是这样做

一个。测试创建资源

<?php
$app->createResource('resource');
$this->assertTrue($app->hasResource('resource'));
$app->deleteResource('resource');
?>

b. testDestroyResource

<?php
$app->createResource('resource');
$app->destroyResource('resource');
$this->assertFalse($app->hasResource('resource'));
?>

【讨论】:

-1 在您的第二种方法中,destroyResource 也依赖于 createResource,但并未明确设置为这样。如果 createResource 失败,UTTesting Framework 会错误地指出 destroyResource 不工作【参考方案8】:

如果测试需要按特定顺序运行,则确实存在问题。每个测试都应该完全独立于其他测试:它可以帮助您进行缺陷定位,并允许您获得可重复(因此可调试)的结果。

查看this site 获取大量想法/信息,了解如何以避免此类问题的方式考虑测试。

【讨论】:

PHPUnit 通过@depends 支持测试依赖。

以上是关于按特定顺序运行 PHPUnit 测试的主要内容,如果未能解决你的问题,请参考以下文章

在 Laravel 8 中运行多个 PHPUnit 测试时出错

phpunit随机重启测试

PHPUnit:从setUp()获取测试类和方法的名称?

PhpUnit 随机卡住 60 秒

PhpStorm 中的 PHPUnit 测试:无法打开文件

复制生产数据以测试数据库以进行 PHPUnit 测试的有效方法