PHPUnit测试分离

PHPUnit测试分离

问题描述:

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

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

让我再解释一下:在我的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()); 
    } 
} 

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

问题的第二部分:是否有可能为测试类或类似的工作提供帮助函数?我的意思是提供登录客户端的方法。这将用于管理功能测试。

+0

为什么评价如此糟糕?我能做些什么来改善我的问题? – SebTM

+0

因为这个问题非常广泛,主要是基于意见的,并且没有明确的答案。你会想要一些更具体的问题,以及你尝试过的一些代码示例。 –

+0

我已经添加了当前的代码并更新了文本。你说得对,这可能是基于意见的,但我没有找到关于如何管理代码的“最佳实践”。这将有助于我从社区体验中受益,因为这里有许多专业开发人员。 – SebTM

现在你的问题更具体一些,我会提供一个答案和一些解释。你为第一次测试做的事情可能会起作用,但不是你应该测试的方式。这不是最好的实践,因为它是绕过单元测试的想法,检查针对单个工作单元的假设。你的测试有几个“单位”的工作正在测试,他们应该都在单独的测试。

这里是一个比较合适的测试来为前两种情况的浓缩例如:

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

它不仅是单元测试的最佳实践,但是还有另外一个需要注意的,当涉及到使用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。你可以把它放在一个单独的测试类中,就像他们的文档一样,或者你可以制作你自己的具有这个功能的类。

TL; DR不要尝试聪明与你的测试/测试的情况下,分开你的测试,并建立辅助功能或基础测试用例类从,如果你重复使用很多相同设置的延伸。

+0

感谢您的帮助!恐怕我可以改进我的问题,以获得像这样的专业和有用的答案:) – SebTM

+0

没问题 - 当您给出您尝试的代码示例时,审阅者确切地知道您的意思并帮助你。 –