不管什么时候,你只要编写一行新的代码,你就有可能引入新的Bug。你应该使用自动测试,该教程将向你显示如何为你的应用程序编写单元测试和功能测试。php
Symfony2测试很大程序上依赖PHPUnit,它的最佳实践,和一些约定。这部分并非PHPUnit自己的文档,但若是你仍是不能理解的话,你能够阅读它优秀的文档 。html
Symfony2使用PHPUnit 3.5.11或以上版本。node
缺省的PHPUnit配置将在你Bundle的Tests/子目录中查找测试:web
<!-- 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 来生成。bootstrap
编写Symfony2单元测试与标准PHPUnit的单元测试没什么不一样。一般推荐将Bundle目录结构复制到Tests/子目录中。所以为Acme\HelloBundle\Model\Article类所写的测试会放置在Acme/HelloBundle/Tests/Model/ArticleTest.php文件中。数组
在单元测试中,自动加载经过src/autoload.php文件是自动启用的(这在phpunit.xml.dist文件中是被缺省配置的)。浏览器
为指定文件或目录运行测试也十分容易:cookie
# 为控制器运行全部测试 $ 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关注度而言,它们与单元测试没什么不一样,除了它们有一个很是特殊的工做流:app
*制做一个请求
*测试响应
*点击连接或提交表单
*测试响应
*修正和重复
请求、点击和提交经过一个知道如何与应用程序通讯客户端来实现。要访问该客户端,你的测试必须继承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();
强烈建议功能测试只测试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);
每次你用Client生成请求时都会返回一个Crawler实例。它容许你遍历HTML文档、选择节点、找到连接和表单。
当你用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.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建立一个在特定测试环境下运行的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。