我将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行复制和粘贴)重复代码。这是否遵循最佳做法?分离单元测试的最佳实践是什么? (或其他措辞:你会怎么做?)
问题的第二部分:是否有可能为测试类或类似的工作提供辅助函数?我的意思是作为示例的方法提供登录的客户端。管理功能测试需要这样做。
答案 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());
}
这里有一些事情。
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提供了一个示例。您可以将它放在单独的测试类中,就像他们的文档一样,或者您可以创建自己的MyBaseTestCase
类,其中包含该函数。
TL; DR 不要试图聪明地使用测试/测试用例,分离测试,并创建辅助函数或基本测试用例类,以便在重用大量的相同的设置。