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行复制重复的代码粘贴&)。这是否遵循最佳实践?分离单元测试的最佳做法是什么? (或其他措辞:你会怎么做?)
问题的第二部分:是否有可能为测试类或类似的工作提供帮助函数?我的意思是提供登录客户端的方法。这将用于管理功能测试。
现在你的问题更具体一些,我会提供一个答案和一些解释。你为第一次测试做的事情可能会起作用,但不是你应该测试的方式。这不是最好的实践,因为它是绕过单元测试的想法,检查针对单个工作单元的假设。你的测试有几个“单位”的工作正在测试,他们应该都在单独的测试。
这里是一个比较合适的测试来为前两种情况的浓缩例如:
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());
}
有几件事情在这里。
- 您担心代码重复和额外的代码行,但我只创建了两个单独的测试,根本没有增加行数。我能够删除
followRedirects()
调用,因为它不适用于这些测试,并且我通过简单地重新创建客户端和爬虫程序来消除了两行克隆,这是不太令人困惑的。 - 使用您的代码只有一个单元测试,但如果该测试失败,则可能出于多种不同原因 - 登录失败,登录成功等。因此,如果该测试失败,则必须筛选错误消息并找出你的系统哪部分失败。通过分离测试,当测试失败时,您只需通过测试名称即可知道出了什么问题。
- 您可以通过分离测试来消除一些冗余代码注释:
// 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不要尝试聪明与你的测试/测试的情况下,分开你的测试,并建立辅助功能或基础测试用例类从,如果你重复使用很多相同设置的延伸。
感谢您的帮助!恐怕我可以改进我的问题,以获得像这样的专业和有用的答案:) – SebTM
没问题 - 当您给出您尝试的代码示例时,审阅者确切地知道您的意思并帮助你。 –
为什么评价如此糟糕?我能做些什么来改善我的问题? – SebTM
因为这个问题非常广泛,主要是基于意见的,并且没有明确的答案。你会想要一些更具体的问题,以及你尝试过的一些代码示例。 –
我已经添加了当前的代码并更新了文本。你说得对,这可能是基于意见的,但我没有找到关于如何管理代码的“最佳实践”。这将有助于我从社区体验中受益,因为这里有许多专业开发人员。 – SebTM