test
Every time you write an extra line of code, you are adding potential new bugs. In order to build better, more reliable programs, you need to use functional and unit tests on your code.
PHPUnit Test Framework¶
Symfony integrates an independent class library - named PHPUnit - to bring you a rich testing framework. This chapter does not cover PHPUnit itself, but it has its ownExcellent documentation.
It is recommended to use the latest stable version of PHPUnit,is installed with PHAR.
Every test - whether it is a unit test or a functional test - is a PHP class, stored in your Bundle'sTests/
subdirectory. If you follow this principle, then when running all program-level tests, just run the following command:
1 |
$ phpunit
|
PHPunit is configured through thephpunit.xml.dist
file in the Symfony root directory.
Code coverage can be generated with the—coverage-*
option. To view help information, use—help
to display more content.
Unit testing¶
Unit testing is a test for a single PHP class. This class is also called "unit (unit)". If you need to test the overall behavior of your application hierarchy, see Functional Tests (#catalog2).
Writing Symfony unit tests is no different than writing standard PHPUnit unit tests. Suppose, you have an extremely simple class namedCalculator
located in theUtil/
directory of the app bundle:
// src/AppBundle/Util/Calculator.phpnamespace AppBundle\Util; class Calculator{ public function add($a, $b) { return $a + $b; }}
To test it, create aCalculatorTest
File to thetests/AppBundle/Util
directory of your bundle:
// tests/AppBundle/Util/CalculatorTest.phpnamespace Tests\AppBundle\Util; use AppBundle\Util\Calculator; class CalculatorTest extends \PHPUnit_Framework_TestCase{ public function testAdd() { $calc = new Calculator(); $result = $calc->add(30, 12); // assert that your calculator added the numbers correctly! $this->assertEquals(42, $result); }}
According to the agreement ,Tests/AppBundle
directory should be copied to the directory where the unit test files under your bundle are located. Therefore, if the class you want to test is located in thesrc/AppBundle/Util
directory, then put the test code in thetests/AppBundle/Util/
directory.
Just like your real program – autoloading is automatically enabled and completed through thebootstrap.php.cache
file (this part of the configuration The default is in the app/phpunit.xml.dist file)
The test for the specified file or directory is also very simple:
# run all tests of the application# 运行程序中的所有测试$ phpunit # run all tests in the Util directory# 运行指定目录下的所有测试$ phpunit tests/AppBundle/Util # run tests for the Calculator class# 仅运行Calculator类的测试$ phpunit tests/AppBundle/Util/CalculatorTest.php # run all tests for the entire Bundle# 运行整个bundle的测试$ phpunit tests/AppBundle/
Functional test¶
Functional tests check the integration of different levels of the program (from routing to views). These levels themselves do not change due to the intervention of PHPUnit, but they have unique workflows:
Make a request;
Test response (response);
Click a link or submit a form;
Test response;
Clear and repeat.
Your first functional test¶
The functional test is stored in theTest/AppBundle/Controller
directory A normal PHP file. If you want to test pages handled by yourPostController
, first create a new PostControllerTest.php file and inherit a specialWebTestCase
class.
As a routine, this test might look like the following code:
// tests/AppBundle/Controller/PostControllerTest.phpnamespace Tests\AppBundle\Controller; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class PostControllerTest extends WebTestCase{ public function testShowPost() { $client = static::createClient(); $crawler = $client->request('GET', '/post/hello-world'); $this->assertGreaterThan( 0, $crawler->filter('html:contains("Hello World")')->count() ); }}
WebTestCaseclass starts the program kernel. In most cases, this is done automatically. But if the kernel is located in a non-standard directory, you need to adjust the
phpunit.xml.distfile and reset the
KERNEL_DIRenvironment variable to your kernel directory:
createClient()The method returns a client, which can be used to simulate a browser so that you can crawl website pages:
request() method
(refer toMore request methods) returns aCrawler
object, Used to select elements from response content and complete actions such as clicking a link or submitting a form.
The response information must be in XML or HTML document format for the crawler to work. If you need raw content without document format, please use$client->getResponse()->getContent()
.
When you need to click a link, first use the grabber to select it. You can use XPath expressions or CSS pickers to complete the selection, and then use client behavior to click it:
$link = $crawler ->filter('a:contains("Greet")') // 选择所有含有"Greet"文本之链接 ->eq(1) // 结果列表中的第二个 ->link() // 点击它; $crawler = $client->click($link);
Submitting a form is very similar: select a form button, optionally override the values of some form items, and then submit the corresponding form:
$form = $crawler->selectButton('submit')->form(); // 设置一些值$form['name'] = 'Lucas';$form['form_name[subject]'] = 'Hey there!'; // 提交表单$crawler = $client->submit($form);
Form Uploads can also be handled, and it also contains methods for filling in different types of form fields (such asselect()
andtick()
). For details, please refer to the following sectionForms.
Now you can easily move through your program, using assertions to test whether the page behaves as you expect. Use a Crawler to make assertions act on the DOM.
// Assert that the response matches a given CSS selector.// 断言响应内容匹配到一个指定的CSS拾取器$this->assertGreaterThan(0, $crawler->filter('h1')->count());
Or test the response content directly, if you just want to assert whether the content contains certain text, or just know that the response content is not in XML/HTML format:
$this->assertContains( 'Hello World', $client->getResponse()->getContent());
Use Test Client¶
Test The client (test client) simulates an HTTP client, just like a browser, and can generate requests for your Symfony program.
1 |
$crawler = $client->request('GET', '/post/hello-world'); |
request()
The method accepts an HTTP request parameter and a URL parameter, and returns a Crawler instance.
For functional testing, hard-coding the request link is the best practice. If you use Symfony routing when generating URLs in your tests, it will not be able to detect any changes to the URL page, which may result in a compromised user experience.
Use a scraper to find DOM elements in the response content. These elements can be used in occasions such as clicking links or submitting forms:
$link = $crawler->selectLink('Go elsewhere...')->link();$crawler = $client->click($link); $form = $crawler->selectButton('validate')->form();$crawler = $client->submit($form, array('name' => 'Fabien'));
Theclick()
method and thesubmit()
method here will both Returns acrawler
crawler object. These methods are the best way to browse your application's pages because they do a lot of things for you, like detect HTTP request patterns from forms, or provide you with a nice API for file uploads.
You will learn more about Link and Form objects in the followingCrawlersections.
request
method can also be used to directly simulate form submission, or to perform more complex requests. Some useful examples:
// Directly submit a form (but using the Crawler is easier!)// 直接提交表单(但是使用Crawler更容易些)$client->request('POST', '/submit', array('name' => 'Fabien')); // Submit a raw JSON string in the request body// 在请求过程中提交一个JSON原生串$client->request( 'POST', '/submit', array(), array(), array('CONTENT_TYPE' => 'application/json'), '{"name":"Fabien"}'); // Form submission with a file upload// 文件上传表单的提交use Symfony\Component\HttpFoundation\File\UploadedFile; $photo = new UploadedFile( '/path/to/photo.jpg', 'photo.jpg', 'image/jpeg', 123);$client->request( 'POST', '/submit', array('name' => 'Fabien'), array('photo' => $photo)); // Perform a DELETE request and pass HTTP headers// 执行一个DELETE请求并传递HTTP头$client->request( 'DELETE', '/post/12', array(), array(), array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word'));
As a final reminder, you can force each request to be executed in their own PHP process to avoid side effects when there are multiple clients in the same script.
##1 |
$crawler = $client->request('GET', '/post/hello-world'); |
Browsing¶
Client object supports many operations in real browsers:
$client->back();$client->forward();$client->reload(); // Clears all cookies and the history// 清除所有cookie和历史记录$client->restart();
Use internal Object¶
getInternalRequest() and getInternalResponse() methods were introduced from Symfony 2.3. If you use the client to test your program, you may want to use some objects inside the client:
$history = $client->getHistory();$cookieJar = $client->getCookieJar();
You can also get the object related to the most recent request:
// the HttpKernel request instance// HttpKernel的请求实例$request = $client->getRequest(); // the BrowserKit request instance// 浏览器组件的请求实例$request = $client->getInternalRequest(); // the HttpKernel response instance// HttpKernel的响应实例$response = $client->getResponse(); // the BrowserKit response instance// 浏览器组件的响应实例$response = $client->getInternalResponse(); $crawler = $client->getCrawler();
If your request is not running in isolation, you can also useContainer
(container) andKernel
(kernel):
$container = $client->getContainer();$kernel = $client->getKernel();
Using containers¶
It is strongly recommended that functional testing only tests the response. But in certain rare cases, you may want to use internal objects to write assertions. At this time, you can call Dependency Injection Container (dependency injection container, that is, service container):
##1 |
$client->insulate(); |
1 |
$container = $client->getContainer();
|
Note that if you run the client in isolation or use an HTTP layer, the above function will not work . You can view the list of available services in the program through thedebug:container
command line tool.
Before Symfony2.6, this command was
container:debug
.
If the information you need to check is available in a profiler, profiler should be used instead of container.
Using analysis data¶
For each request, you can use the Symfony profiler to collect information about how the request is processed internally. information processed. For example, analyzers are often used to verify whether the number of database queries during the loading process of a specified page is less than a given value.
In order to get the Profiler of the latest request, follow the example:
// enable the profiler for the very next request// 为接下来的请求开启profiler$client->enableProfiler(); $crawler = $client->request('GET', '/profiler'); // get the profile 得到分析器$profile = $client->getProfile();
For some details on using the profiler in the test, please refer toHow to use it in functional testing Using the analyzerarticle.
Redirect¶
When a request returns a redirect response, the client does not automatically follow up. You can detect the response message and then force follow through thefollowRedirect()
method:
1 |
$crawler = $client->followRedirect();
|
If you need the client to automatically follow all redirects, you can force it to do so by usingfollowRedirects()
method:
1 |
$client->followRedirects();
|
willfalse
Passed to thefollowRedirects()
method, the redirect will no longer be followed:
1 |
$client->followRedirects(false);
|
Crawler¶
Every time you make a request through the client, a Crawler instance is returned. Scrapers allow you to traverse HTML documents, pick up DOM nodes, and find links and forms.
Traversing¶
Just like JQuery, the crawler has various methods to traverse the DOM of an HTML/XML document. For example, the following method finds allinput[type=submit]
elements, selects the last one on the page, and then selects its adjacent parent element:
$newCrawler = $crawler->filter('input[type=submit]') ->last() ->parents() ->first();
There are many other methods You can use:
filter('h1.title')
- Matches all nodes of the CSS picker.
filterXpath('h1')
- All nodes matching the XPath expression (expression).
eq(1)
- The node specifying the index.
first()
- The first node.
last()
- The last node.
siblings()
- Siblings.
nextAll()
- All subsequent siblings.
previousAll()
- All previous siblings.
parents()
- Returns all parent nodes.
children()
- Returns all child nodes.
reduce($lambda)
- All nodes when the anonymous function $lambda does not return false.
Since each of the above methods returns aCrawler
grabber object, you can reduce the code of the node picking process by chaining calls to each method:
$crawler ->filter('h1') ->reduce(function ($node, $i) { if (!$node->getAttribute('class')) { return false; } }) ->first();
You can use thecount()
function to get the number of nodes stored in a Crawler:count($crawler)
Extract information¶
The crawler can extract information from nodes:
// 返回第一个节点的属性值$crawler->attr('class'); // 返回第一个节点的节点文本$crawler->text(); // Extracts an array of attributes for all nodes// (_text returns the node value)// returns an array for each element in crawler,// each with the value and href// 提取一个由所有节点的属性组成的数组// (_text返回节点值)// 返回一个由抓取器中的每一个元素所组成的数组// 每个元素有value和href$info = $crawler->extract(array('_text', 'href')); // Executes a lambda for each node and return an array of results// 对每个节点执行一次lambda函数(即匿名函数),最后返回结果数组$data = $crawler->each(function ($node, $i) { return $node->attr('href');});
Links¶
To select a link, you can use the above traversal method, or use the more convenientselectLink()
shortcut method:
1 |
$crawler->selectLink('Click here');
|
This will select all links that contain the given text, or clickable images whosealt
attributes contain the given text. Similar to other filtering methods, it also returns aCrawler
object.
Once you select a link (link), you can use a special link object (link
object), which is a method specifically designed to handle links (similar togetMethod()
orgetUri()
are all helper methods/helpful methods). To click a link, use the Client'sclick()
method and pass the link object in:
$link = $crawler->selectLink('Click here')->link(); $client->click($link);
Form¶
Form Can be selected via their buttons, which can be selected using the selectButton() method, just like links:
##1 |
$buttonCrawlerNode = $crawler->selectButton('submit'); |
method can select thebutton
(button) label and the label where submit (submit) is located (i.e.input
Label). It uses several parts of the button to find them:
- value
The value attribute value
The id or alt attribute value for images
- id
##button
The id or name attribute value for button tags When you have a After the crawler containing the button, call the
- form()
method to get the form instance that owns the button node.
##1
$form = $buttonCrawlerNode->form();
$form = $buttonCrawlerNode->form(array( 'name' => 'Fabien', 'my_form[subject]' => 'Symfony rocks!',));
When calling the
form()
method, you can also pass in an array containing the form field values to replace the default: |
When you want to simulate a specific HTTP form submission method, pass it to the second parameter: |
1
$form = $buttonCrawlerNode->form(array(), 'DELETE');
Client is responsible for submitting the form instance:
The value of the field can be passed to the second parameter of the submit() method:
$client->submit($form, array( 'name' => 'Fabien', 'my_form[subject]' => 'Symfony rocks!',));
For more complex situations, the form instance can be used as an array to set it separately. The value corresponding to each field:
// 改变某个字段的值/Change the value of a field$form['name'] = 'Fabien';$form['my_form[subject]'] = 'Symfony rocks!';
There is also a very useful API that can manage their values according to the field type:
// 选取一个option单选或radio单选$form['country']->select('France'); // 选取一个checkbox$form['like_symfony']->tick(); // 上传一个文件$form['photo']->upload('/path/to/lucas.jpg');
If you purposely want to select "invalid" select/radio values, refer toSelecting invalid Choice Values.
You can get the form field values that are about to be submitted by calling thegetValues()
method of the Form object. Uploaded files can also be obtained from a detached array returned by thegetFiles()
method. ThegetPhpValues()
andgetPhpFiles()
methods can also return submitted fields, but in PHP format (it takes the key with square brackets – such asmy_form[ subject]
– converted into a PHP array).
Add or delete forms to Collection¶
If you useCollection of Forms (form collection), you Cannot add a$form['task[tags][0][name]'] = 'foo
field to an existing form. This results in anUnreachable field "…"
error. Because$form
can only be used to set existing fields. In order to add a new field, you have to add the value to a primitive array:
// Get the form. 取得表单$form = $crawler->filter('button')->form(); // Get the raw values. 取得原始数值$values = $form->getPhpValues(); // Add fields to the raw values. 给原始数组添加新字段$values['task']['tag'][0]['name'] = 'foo';$values['task']['tag'][1]['name'] = 'bar'; // Submit the form with the existing and new values.// 提交表单,包括既有值,以及新值$crawler = $this->client->request($form->getMethod(), $form->getUri(), $values, $form->getPhpFiles()); // The 2 tags have been added to the collection.// 2个新标签被添加到collection中$this->assertEquals(2, $crawler->filter('ul.tags > li')->count());
Heretask[tags][0][name]
is the field created by JavaScript name.
You can remove an existing field, such as a tag:
// Get the values of the form. 取得表单数据$values = $form->getPhpValues(); // Remove the first tag. 移除第一个标签unset($values['task']['tags'][0]); // Submit the data. 提交数据$crawler = $client->request($form->getMethod(), $form->getUri(), $values, $form->getPhpFiles()); // The tag has been removed. 标签已被移除$this->assertEquals(0, $crawler->filter('ul.tags > li')->count());
Configuration test¶
The Client used in functional testing creates a Kernel for running in a specialtest
test environment. Since Symfony loadsapp/config/config_test.yml
when testing, you can adjust any "program-level" settings used for testing.
For example, the default Swift Mailer is set not to send emails in a test environment. The relevant options in the configuration file are set like this:
PHP:// app/config/config_test.php // ...$container->loadFromExtension('swiftmailer', array( 'disable_delivery' => true,));
XML:
YAML:# app/config/config_test.yml # ...swiftmailer: disable_delivery: true
You can also use a completely different environment, or override the default debug mode (true
) by passing the relevant options to thecreateClient()
method:
$client = static::createClient(array( 'environment' => 'my_test_env', 'debug' => false,));
If your program requires some HTTP headers to run, pass them to the second parameter of the createClient() method:
$client = static::createClient(array(), array( 'HTTP_HOST' => 'en.example.com', 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',));
You can also pass them in each request The basic process is to overwrite the HTTP header:
$client->request('GET', '/', array(), array(), array( 'HTTP_HOST' => 'en.example.com', 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',));
PHPUnit configuration¶
Each program has its own PHPUnit configuration, which exists inapp/phpunit. xml.dist
file. You can edit this file to change the default values, or create aapp/phpunit.xml
file that sets test options only for your local machine.
Store theapp/phpunit.xml.dist
file in your repository and ignore theapp/phpunit.xml
file.
The default is only stored in the standard directory (src//Bundle/Tests, src//Bundle/Bundle/Tests, src/ *Bundle/Tests) The tests in your custom bundle can be executed by the phpunit command. This is already configured in the app/phpunit.xml.dist file:
../src/*/*Bundle/Tests ../src/*/Bundle/*Bundle/Tests ../src/*Bundle/Tests
But you can easily add more directories. For example, the following configuration adds tests in the custom directory lib/tests:
../lib ../lib/tests