无论何时,你只要编写一行新的代码,你就有可能引入新的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。
本文转自 firehare 51CTO博客,原文链接:http://blog.51cto.com/firehare/586500,如需转载请自行联系原作者