【问题标题】:PHPUnit separation of tests [closed]PHPUnit测试分离[关闭]
【发布时间】:2016-02-27 22:09:34
【问题描述】:

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

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

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

<?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 行复制和粘贴)重复代码。这是否遵循最佳实践?分离单元测试的最佳实践是什么? (或其他措辞:你会怎么做?)

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

【问题讨论】:

  • 为什么评价这么差?我该怎么做才能使我的问题变得更好?
  • 因为这个问题非常广泛,大多是基于意见的,并且没有明确的答案。您可能想要一个更具体的问题,其中包含一些您尝试过的代码示例。
  • 我已经添加了我当前的代码并稍微更新了文本。你是对的,这可能是基于意见的,但我没有找到关于如何管理代码的“最佳实践”。这将帮助我从社区经验中受益,因为这里有许多专业的开发人员。

标签: php unit-testing symfony


【解决方案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. 您可以通过分离测试来消除一些冗余代码 cmets:不再需要 // Set wrong user-data,因为测试本身称为 testLoginFailure()

它不仅是单元测试的最佳实践,而且在使用 WebTestCase 时还有另一个警告,因为您希望所有测试都被隔离。我试图创建一个整个类都可以使用的静态$client 变量,我认为如果我只实例化一个实例,我会节省内存/时间,但是当你开始运行多个测试时,这会导致不可预知的行为。您希望您的测试独立进行。

如果您真的想消除冗余代码,您还可以使用 setUp() and tearDown() 函数并在每个请求之前实例化 $this-&gt;client$this-&gt;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 如果您重用大量相同的设置。

【讨论】:

  • 感谢您的帮助!恐怕我可以改进我的问题以获得这样的专业和有用的答案:)
  • 没问题 - 当你给出你尝试过的代码示例时,审阅者更容易准确地了解你的意思并为你提供帮助。
猜你喜欢
  • 1970-01-01
  • 2012-08-20
  • 2013-11-27
  • 2010-09-21
  • 2020-11-03
  • 2015-12-29
  • 1970-01-01
  • 2018-08-31
  • 1970-01-01
相关资源
最近更新 更多