无论何时,你只要编写一行新的代码,你就有可能引入新的Bug。你应该使用自动测试,该教程将向你显示如何为你的应用程序编写单元测试和功能测试。

测试框架

Symfony2测试很大程序上依赖PHPUnit,它的最佳实践,和一些约定。这部分并不是PHPUnit本身的文档,但如果你还是不能理解的话,你可以阅读它优秀的文档

Symfony2使用PHPUnit 3.5.11或以上版本。

缺省的PHPUnit配置将在你Bundle的Tests/子目录中查找测试:

<!-- app/phpunit.xml.dist -->

<phpunit bootstrap="../src/autoload.php">
    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>../src/*/*Bundle/Tests</directory>
        </testsuite>
    </testsuites>

    ...
</phpunit>

对指定应用程序运行测试套件是简单的:

# 在命令行指定配置目录
$ phpunit -c app/

# 或者在应用程序目录中运行phpunit
$ cd app/
$ phpunit

代码的覆盖范围可以通过 --coverate-html 来生成。

 

单元测试

编写Symfony2单元测试与标准PHPUnit的单元测试没什么不同。通常推荐将Bundle目录结构复制到Tests/子目录中。因此为Acme\HelloBundle\Model\Article类所写的测试会放置在Acme/HelloBundle/Tests/Model/ArticleTest.php文件中。

在单元测试中,自动加载通过src/autoload.php文件是自动启用的(这在phpunit.xml.dist文件中是被缺省配置的)。

为指定文件或目录运行测试也十分容易:

# 为控制器运行所有测试
$ phpunit -c app src/Acme/HelloBundle/Tests/Controller/

# 为模型运行所有测试
$ phpunit -c app src/Acme/HelloBundle/Tests/Model/

# 为Article类运行测试
$ phpunit -c app src/Acme/HelloBundle/Tests/Model/ArticleTest.php

# 为整个Bundle运行所有测试
$ phpunit -c app src/Acme/HelloBundle/

功能测试

功能测试检查应用程序不同层的集成(从路由到视图)。就PHPUnit关注度而言,它们与单元测试没什么不同,除了它们有一个非常特殊的工作流:

*制作一个请求
*测试响应
*点击链接或提交表单
*测试响应
*修正和重复

请求、点击和提交通过一个知道如何与应用程序通信客户端来实现。要访问该客户端,你的测试必须继承Symfony2的WebTestCase类。沙箱提供了一个HelloControoler控制器简单的功能测试,如下所示:

// src/Acme/HelloBundle/Tests/Controller/HelloControllerTest.php
namespace Acme\HelloBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class HelloControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = $this->createClient();

        $crawler = $client->request('GET', '/hello/Fabien');

        $this->assertTrue($crawler->filter('html:contains("Hello Fabien")')->count() > 0);
    }
}

createClient()方法返回一个与当前应用程序绑定的客户端

$crawler = $client->request('GET', 'hello/Fabien');

request()方法返回一个Crawler对象,该对象可以用于在Response中选择元素。可以用来点击链接,也可以用来提交表单。

当Response的内容是XML或HTML文档,可以只使用Crawler对象。对于内容的其它类型,可以通过$client->getResponse()->getContent()来得到内容。

点击链接:首先选择Crawler使用XPath表达式或CSS选择器的链接,然后用Client去点击它:

$link = $crawler->filter('a:contains("Greet")')->eq(1)->link();

$crawler = $client->click($link);

提交表单也非常简单;选择一个表单按钮,你可以覆写一些表单的值,然后提交相应的表单:

$form = $crawler->selectButton('submit')->form();

// 设置一些值
$form['name'] = 'Lucas';

// 提交表单
$crawler = $client->submit($form);

每个表单项根据它的类型都有相对应的方法:

// 填充一个input项
$form['name'] = 'Lucas';

// 选择一个option或radio
$form['country']->select('France');

// 勾掉一个检查框
$form['like_symfony']->tick();

// 上传一个文件
$form['photo']->upload('/path/to/lucas.jpg');

如果不想一次改变一个表单项,你也可以发送一个数组给submit()方法:

$crawler = $client->submit($form, array(
    'name'         => 'Lucas',
    'country'      => 'France',
    'like_symfony' => true,
    'photo'        => '/path/to/lucas.jpg',
));

现在你可以很轻易浏览应用程序,使用声明去测试看看程序实际上是否按你所预期的执行。使用Crawler在DOM上执行中断:

// 声明响应匹配指定的CSS选择器
$this->assertTrue($crawler->filter('h1')->count() > 0);

或者,如果你只是想声明内容包含一些文本,test可以直接针对Response内容。如果Response不是一个XML/HTML文档,则无法实现。(这段翻得不畅,留下英文原文吧)

Or, test against the Response content directly if you just want to assert that the content contains some text, or if the Response is not an XML/HTML document:

$this->assertRegExp('/Hello Fabien/', $client->getResponse()->getContent());

有用的声明

在一段时间之后,你会注意到你总是写同一类型的声明。为了你更快地开始,这里有一个常用的声明列表:

// 声明响应匹配指定的CSS选择器。
$this->assertTrue($crawler->filter($selector)->count() > 0);

// 声明响应匹配指定的CSS选择器N次
$this->assertEquals($count, $crawler->filter($selector)->count());

// 声明响应头有给定的值
$this->assertTrue($client->getResponse()->headers->contains($key, $value));

// 声明响应内容匹配正则表达式
$this->assertRegExp($regexp, $client->getResponse()->getContent());

// 声明响应状态码
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertTrue($client->getResponse()->isNotFound());
$this->assertEquals(200, $client->getResponse()->getStatusCode());

// 声明响应状态码是重定向
$this->assertTrue($client->getResponse()->isRedirected('google.com'));

测试客户端

测试客户端模拟类似浏览器的HTTP客户端。

测试客户端基于BrowserKit和Crawler组件。

制造请求

客户端知道如何制作一个发往Symfony2应用的请求:

$crawler = $client->request('GET', '/hello/Fabien');

request()方法将HTTP方法和URL作为参数,然后返回一个Crawler实体。

使用Crawler去发现Response中的DOM元素。这些元素随后可以用于点击链接和提交表单:

$link = $crawler->selectLink('Go elsewhere...')->link();
$crawler = $client->click($link);

$form = $crawler->selectButton('validate')->form();
$crawler = $client->submit($form, array('name' => 'Fabien'));

click()和submit()方法都返回一个Crawler对象。这些方法浏览应用程序并隐藏大量细节的最好方式。例如,当你提交一个表单时,它自动匹配HTTP方法和表单URL、它给你一个设计良好的API去上传文件、它合并表单缺省值和提交值,等等储如此类。

在接下来的Crawler章节中,你将学到更多关于Link和Form对象。

但你也可以使用request()方法的附加参数来模拟表单提交和复杂请求:

// 表单提交
$client->request('POST', '/submit', array('name' => 'Fabien'));

// 带文件上传的表单提交
$client->request('POST', '/submit', array('name' => 'Fabien'), array('photo' => '/path/to/photo'));

// 指定HTTP头
$client->request('DELETE', '/post/12', array(), array(), array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word'));

当一个请求返回一个重定向响应,客户端会自动遵循它。这个行为可以被followRedirects()方法改变:

$client->followRedirects(false);

当客户端遵循响应进行重定向时,你可以使用followRedirect()迫强使它进行重定向:

$crawler = $client->followRedirect();

最后但并非不重要,当在同一脚本使用多个客户端工作时,你可以迫使每个请求都在它自己的PHP进程中执行以避免产生副作用。

$client->insulate();

浏览

客户端支持许多实际浏览器的操作

$client->back();
$client->forward();
$client->reload();

// 清除所有cookies和浏览历史
$client->restart();

访问内部对象

如果你使用客户端去测试你的应用程序,你也许想去访问客户端的内部对象:

$history   = $client->getHistory();
$cookieJar = $client->getCookieJar();

你也可以得到最后请求相应的对象:

$request  = $client->getRequest();
$response = $client->getResponse();
$crawler  = $client->getCrawler();

如果你的请求没有被隔离,你也可以访问Container和Kernel:

$container = $client->getContainer();
$kernel    = $client->getKernel();

访问Container

强烈建议功能测试只测试Response。但在几种非常罕见的情况下,你也许想要访问一些内部对象对编写声明。在这种情况下,你可以访问依赖注入容器:

$container = $client->getContainer();

警告:如果你隔离了客户端或使用HTTP层,它将不能工作。

如果你所需信息被分析器检出是可用的话,那么用它们代替。

访问分析器数据

要让声明数据被分析器收集,你可以所下所示得到分析器:

use Symfony\Component\HttpKernel\Profiler\Profiler;

$profiler = new Profiler();
$profiler = $profiler->loadFromResponse($client->getResponse());

重定向

缺省状态下,客户端遵循HTTP重定向。但如果你想在重定向之前得到Response并将其重定向给自己,那么调用followRedirects()方法:

$client->followRedirects(false);

$crawler = $client->request('GET', '/');

// 用重定向响应做一些事

// 手工重定向
$crawler = $client->followRedirect();

$client->followRedirects(true);

Crawler

每次你用Client生成请求时都会返回一个Crawler实例。它允许你遍历HTML文档、选择节点、找到链接和表单。

创建一个Crawler实例

当你用Client生成请求时,一个Crawler实例将会自动为你创建。但你也可以很容易地自行创建:

use Symfony\Component\DomCrawler\Crawler;

$crawler = new Crawler($html, $url);

构造函数有两个参数:第2个参数是为链接和表单生成绝对URL的URL;第1个参数可以使用以下内容:

* HTML文档
* XML文档
* DOMDocument实例
* DOMNodeList实例
* 上述元素的数组

创建之后,你可以添加更多的节点:

方法 描述
addHTMLDocument() HTML文档
addXMLDocument() XML文档
addDOMDocument() DOMDocument实例
addDOMNodeList() DOMNodeList实例
addDOMNode() DOMNode实例
addNodes() 上述元素的数组
add() 接受上述任一元素

遍历

象jQuery一样,Crawler有方法去遍历HTML/XML文档的DOM:

方法 描述
filter('h1') 匹配CSS选择器的节点
filterXpath('h1') 匹配XPath表达式的节点
eq(1) 指定索引的节点
first() 第1个节点
last() 最后1个节点
siblings() 兄弟节点
nextAll() 所有后面的兄弟节点
previousAll() 所有前面的兄弟节点
parents() 父节点
children() 子节点
reduce($lambda) 所有被调用后不返回false的节点

你可以通过链式方法调用来迭代缩小你选择的节点,注意你每个匹配节点用的方法都需要返回一个新的Crawler实例。

$crawler
    ->filter('h1')
    ->reduce(function ($node, $i)
    {
        if (!$node->getAttribute('class')) {
            return false;
        }
    })
    ->first();

使用count()函数得到保存在Crawler:count($crawler)中的节点数。

提取信息

Crawler可以从节点提取信息:

// 返回第1个节点的属性值
$crawler->attr('class');

// 返回第1个节点的节点值
$crawler->text();

// 提取所有节点的属性数组(_text返回节点值)
$crawler->extract(array('_text', 'href'));

// 为每个节点运行lambda,并返回结果数组
$data = $crawler->each(function ($node, $i)
{
    return $node->getAttribute('href');
});

链接

你可以选择带有遍历方法的链接,但selectLink()快捷方法更为方便:

$crawler->selectLink('Click here');

它选择包含指定文本的链接,或者alt属性包含指定文本的可点击图片。

Client对象的click()方法驱动一个被link()方法返回的Link实例:

$link = $crawler->link();

$client->click($link);

links()方法为所有节点返回一个Link对象的数组。

表单

你选择有着selectButton()方法的表单:

$crawler->selectButton('submit');

注意我们选择了表单按钮而不是表单,因为表单可以有几个按钮;如果你使用遍历API,那么注意你必须发现按钮。

selectButton()方法可以选择按钮标签并提交input标签;这儿有一些发现它们的技巧:

* 值,属性的值
* 图片的id或alt属性
* 按钮标签的id或name属性

当你有一个代表按钮的节点,调用form()方法去得到一个Form实例,因为表单包含按钮节点:

$form = $crawler->form();

当调用form()方法时,你也可以发送一个覆写缺省值的那些表单项值的数组:

$form = $crawler->form(array(
    'name'         => 'Fabien',
    'like_symfony' => true,
));

如果你想为表单模拟一个特定的HTTP方法,将其作为第2个参数:

$form = $crawler->form(array(), 'DELETE');

Client可以提交一个Form实例:

$client->submit($form);

表单项的值也可以作为submit()方法的第2个参数发送:

$client->submit($form, array(
    'name'         => 'Fabien',
    'like_symfony' => true,
));

更复杂的情况,使用Form实例,并用一个数组来设置每个单独表单项的值:

// 改变表单项的值
$form['name'] = 'Fabien';

也有设计良好的API按照表单项的类型去操作它的值:

// 选择一个option或radio
$form['country']->select('France');

// 勾选一个检查框
$form['like_symfony']->tick();

// 上传一个文件
$form['photo']->upload('/path/to/lucas.jpg');

你可以通过调用getValues()方法得到将提交的值。被上传的文件也可以通过getFiles()返回的数组中得到。getPhpValues()和getPhpFiles()也返回被提交的值,但是以PHP格式返回的(它将方括号中的关键词转换成PHP数组)。

 

测试配置


 

PHPUnit配置

每个应用程序都有它自己的PHPUnit配置,它们被保存在phpunit.xml.dist文件中。你可以编辑这个文件以改变缺省值或者创建phpnit.xml文件去为你的本地机调整配置。

在你的代码库中保存phpunit.xml.dist文件,并忽略phpunit.xml文件。

缺省情况下,测试只被保存在那些通过运行phpunit命令的“标准”Bundle中,(标准是指测试位于Vendor\*Bundle\Tests名称空间)。但你可以很方便地添加更多的名称空间。例如,下面的配置将测试添加在安装的第三方Bundle中。

<!-- hello/phpunit.xml.dist -->
<testsuites>
    <testsuite name="Project Test Suite">
        <directory>../src/*/*Bundle/Tests</directory>
        <directory>../src/Acme/Bundle/*Bundle/Tests</directory>
    </testsuite>
</testsuites>

为了包含代码范围中的其它名称空间,也可以编辑<filter>段:

<filter>
    <whitelist>
        <directory>../src</directory>
        <exclude>
            <directory>../src/*/*Bundle/Resources</directory>
            <directory>../src/*/*Bundle/Tests</directory>
            <directory>../src/Acme/Bundle/*Bundle/Resources</directory>
            <directory>../src/Acme/Bundle/*Bundle/Tests</directory>
        </exclude>
    </whitelist>
</filter>

Client配置

通过功能测试使用的Client创建一个在特定测试环境下运行的Kernel,因此你可以如你所愿地调整它:

# app/config/config_test.yml
imports:
    - { resource: config_dev.yml }

framework:
    error_handler: false
    test: ~

web_profiler:
    toolbar: false
    intercept_redirects: false

monolog:
    handlers:
        main:
            type:  stream
            path:  %kernel.logs_dir%/%kernel.environment%.log
            level: debug

你也可以改变缺省环境(test),通过覆写调试模式(true),并将其做为选项发送给createClient()方法:

$client = $this->createClient(array(
    'environment' => 'my_test_env',
    'debug'       => false,
));

如果你的应用程序是根据一些HTTP头来运行的话,那么将它们作为第2个参数发送给createClient():

$client = $this->createClient(array(), array(
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));

你也可以覆写每个请求的HTTP头。

$client->request('GET', '/', array(), array(
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));

覆写test.client.class参数或定义一个test.client服务来提供你自己的Client。