PHPUnit分离测试

时间:2016-02-27 22:09:35

标签: php unit-testing symfony

我将Symfony 2.8(最新版)用于Web应用程序,其中可单独使用/重用的应用程序的每个部分都是自己的捆绑包。例如,有一个NewsBundle,GalleryBundle,ContactBundle,AdminBundle(这是一个特例 - 它只是EasyAdminBundle收集特定包提供的特征的包装包),UserBundle(存储用户的FOSUserBundle的子包)实体和模板)

我的问题基本上是,单元测试的最佳结构是什么?

让我再解释一下:在我的UserBundle中,我想为我的FOSUserBundle实现进行测试。我有一个方法测试登录页面(通过HTTP状态代码),登录失败(通过错误消息),登录成功(通过特定代码元素),记住我(通过Cookie),注销(通过页面) -content)

<?php

namespace myNamespace\Admin\UserBundle\Tests;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

/**
 * Class FOSUserBundleIntegrationTest.
 */
class FOSUserBundleIntegrationTest extends WebTestCase
{
    /**
     * Tests the login, login "remember-me" and logout-functionality.
     */
    public function testLoginLogout()
    {
        // Get client && enable to follow redirects
        $client = self::createClient();
        $client->followRedirects();

        // Request login-page
        $crawler = $client->request('GET', '/admin/login');

        // Check http status-code, form && input-items
        $this->assertTrue($client->getResponse()->isSuccessful());
        $this->assertEquals(1, $crawler->filter('form[action="/admin/login_check"]')->count());
        $this->assertEquals(1, $crawler->filter('input[name="_username"]')->count());
        $this->assertEquals(1, $crawler->filter('input[name="_password"]')->count());
        $this->assertEquals(1, $crawler->filter('input[type="submit"]')->count());

        // Clone client and crawler to have the old one as template
        $clientLogin = clone $client;
        $crawlerLogin = clone $crawler;

        // Get form
        $formLogin = $crawlerLogin->selectButton('_submit')->form();

        // Set wrong user-data
        $formLogin['_username'] = 'test';
        $formLogin['_password'] = '123';

        // Submit form
        $crawlerLoginFailure = $clientLogin->submit($formLogin);

        // Check for error-div
        $this->assertEquals(1, $crawlerLoginFailure->filter('div[class="alert alert-error"]')->count());

        // Set correct user-data
        $formLogin['_username'] = 'mmustermann';
        $formLogin['_password'] = 'test';

        // Submit form
        $crawlerLoginSuccess = $client->submit($formLogin);

        // Check for specific
        $this->assertTrue(strpos($crawlerLoginSuccess->filter('body')->attr('class'), 'easyadmin') !== false ? true : false);
        $this->assertEquals(1, $crawlerLoginSuccess->filter('li[class="user user-menu"]:contains("Max Mustermann")')->count());
        $this->assertEquals(1, $crawlerLoginSuccess->filter('aside[class="main-sidebar"]')->count());
        $this->assertEquals(1, $crawlerLoginSuccess->filter('div[class="content-wrapper"]')->count());

        // Clone client from template
        $clientRememberMe = clone $client;
        $crawlerRememberMe = clone $crawler;

        // Get form
        $formRememberMe = $crawlerRememberMe->selectButton('_submit')->form();

        // Set wrong user-data
        $formRememberMe['_username'] = 'mmustermann';
        $formRememberMe['_password'] = 'test';
        $formRememberMe['_remember_me'] = 'on';

        // Submit form
        $crawlerRememberMe = $clientRememberMe->submit($formRememberMe);

        // Check for cookie
        $this->assertTrue($clientRememberMe->getCookieJar()->get('REMEMBERME') != null ? true : false);

        // Loop all links on page
        foreach ($crawlerRememberMe->filter('a')->links() as $link) {
            // Check for logout in uri
            if (strrpos($link->getUri(), 'logout') !== false) {
                // Set logout-link
                $logoutLink = $link;

                // Leave loop
                break;
            }
        }

        // Reuse client to test logout-link
        $logoutCrawler = $clientRememberMe->click($logoutLink);

        // Get new client && crawl default-page
        $defaultPageClient = self::createClient();
        $defaultPageCrawler = $defaultPageClient->request('GET', '/');

        // Check http status-code, compare body-content
        $this->assertTrue($defaultPageClient->getResponse()->isSuccessful());
        $this->assertTrue($logoutCrawler->filter('body')->text() == $defaultPageCrawler->filter('body')->text());
    }
}

所有这些测试都将在一种方法中完成,因为如果我以不同的方法进行,我会有大量(5x4行= 20行复制和粘贴)重复代码。这是否遵循最佳做法?分离单元测试的最佳实践是什么? (或其他措辞:你会怎么做?)

问题的第二部分:是否有可能为测试类或类似的工作提供辅助函数?我的意思是作为示例的方法提供登录的客户端。管理功能测试需要这样做。

1 个答案:

答案 0 :(得分:1)

现在您的问题更具体,我将提供一些解释。您为第一次测试所做的工作可能有效,但不是您应该测试的方式。这不是最佳实践,因为它是在绕过单元测试的想法,检查单个工作单元的假设。你的测试有几个“单位”的工作正在测试,它们都应该在单独的测试中。

以下是前两种情况的更合适测试的简要示例:

public function testLoginForm()
{
    $client     = self::createClient();
    $crawler    = $client->request('GET', '/admin/login');

    $this->assertTrue($client->getResponse()->isSuccessful());
    $this->assertEquals(1, $crawler->filter('form[action="/admin/login_check"]')->count());
    $this->assertEquals(1, $crawler->filter('input[name="_username"]')->count());
    $this->assertEquals(1, $crawler->filter('input[name="_password"]')->count());
    $this->assertEquals(1, $crawler->filter('input[type="submit"]')->count());
}

public function testLoginFailure()
{
    $client     = self::createClient();
    $crawler    = $client->request('GET', '/admin/login');
    $form       = $crawler->selectButton('_submit')->form();

    $form['_username'] = 'test';
    $form['_password'] = '123';

    $crawler = $client->submit($form);

    $this->assertEquals(1, $crawler->filter('div[class="alert alert-error"]')->count());
}

这里有一些事情。

  1. 您担心代码重复和额外的代码行,但我刚刚创建了两个单独的测试,它们根本没有增加行数。我能够删除followRedirects()调用,因为它不适用于那些测试,我通过简单地重新创建客户端和爬虫来消除两行克隆,这样就不那么容易混淆了。
  2. 使用您的代码只有一个单元测试,但如果该测试失败,可能由于多种不同的原因 - 登录失败,登录成功等等。因此,如果该测试失败,您必须筛选错误消息,找出你的系统哪个部分失败了。通过分离测试,当测试失败时,您只需通过测试名称就能知道出了什么问题。
  3. 您可以通过分离测试来消除部分冗余代码注释:不再需要// Set wrong user-data,因为测试本身被称为testLoginFailure()
  4. 它不仅是单元测试的最佳实践,而且在使用WebTestCase时还有另一个警告,因为您希望所有测试都被隔离。我试图创建一个整个类可以使用的静态$client变量,认为如果我只实例化一个实例,我将节省内存/时间,但是当你开始运行多个测试时,这会导致不可预测的行为。您希望您的测试单独进行。

    如果你真的想要消除多余的代码,你也可以使用setUp() and tearDown()函数并在每个请求之前实例化$this->client$this->crawler

    use Symfony\Bundle\FrameworkBundle\Client;
    use Symfony\Component\DomCrawler\Crawler;
    
    /*
     * @var Client
     */
    private $client;
    
    /*
     * @var Crawler
     */
    private $crawler;
    
    /*
     * {@inheritDoc}
     */
    protected function setUp()
    {
        $this->client   = self::createClient();
        $this->crawler  = $this->client->request('GET', '/admin/login');
    }
    
    /*
     * {@inheritDoc}
     */
    protected function tearDown()
    {
        unset($this->client);
        unset($this->crawler);
    }
    

    ...但是你要创建类级代码来声明这些变量,实例化它们并将它们拆除。您还最终添加许多其他代码,这是您首先要避免的。此外,您的整个测试类现在都很僵硬且不灵活,因为您永远不能请求登录页以外的页面。另外,PHPUnit本身声明:

      

    测试用例对象的垃圾收集是不可预测的。

    如果您不记得手动清理测试,则上述说法是关于的。因此,除了上面描述的其他原因之外,您可能会因为这些原因而遇到意外行为。

    关于第二个问题,请确保提供辅助函数或扩展现有的*TestCase类。 Symfony文档甚至为private function that logs in a user提供了一个示例。您可以将它放在单独的测试类中,就像他们的文档一样,或者您可以创建自己的MyBaseTestCase类,其中包含该函数。

    TL; DR 不要试图聪明地使用测试/测试用例,分离测试,并创建辅助函数或基本测试用例类,以便在重用大量的相同的设置。