測試
每當你多寫一行程式碼,你都在增加潛在的新bug。為了打造更好、更好可靠的程序,你需要對程式碼使用功能測試和單元測試。
PHPUnit測試框架 ¶
Symfony整合了一個獨立類別庫 – 名為PHPUnit – 帶給你豐富的測試框架。本章不覆蓋PHPUnit本身,不過它有自己的 極佳文件。
推薦使用最新的穩定版PHPUnit,安裝的是PHAR。
每一個測試– 不管是單元測試(unit test)還是功能測試(functional test) - 都是一個PHP類,存放在你的Bundle的Tests/
子目錄下。如果你遵守這項原則,那麼當執行所有的程式級測試時,只需以下指令即可:
1 | $ phpunit |
PHPunit透過Symfony根目錄下的 phpunit.xml.dist
檔案進行設定。
程式碼覆蓋(code coverage)可以透過 —coverage-*
選項來產生。若要查看幫助訊息,可使用 —help
來顯示更多內容。
單元測試 ¶
單元測試是針對單一PHP類別的測試,這個類別也稱為「單元(unit )」。如果你需要測試你的應用程式層級的整體行為,請參考[功能測試] (#catalog2)(Functional Tests)。
編寫Symfony單元測試和編寫標準的PHPUnit單元測試並無不同。假設,你有一個極其簡單的類,類別名稱是Calculator
,位於app bundle的Util/
目錄下:
// src/AppBundle/Util/Calculator.phpnamespace AppBundle\Util; class Calculator{ public function add($a, $b) { return $a + $b; }}
為了測試它,創建一個CalculatorTest
檔案到你的bundle的tests/AppBundle/Util
目錄下:
// 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); }}
#根據約定, Tests/AppBundle
目錄應該複製你的bundle下的單元測試檔案所在的目錄。因此,如果你要測試的類別位於 src/AppBundle/Util
目錄下,那麼就把測試程式碼放在 tests/AppBundle/Util/
目錄下。
就像你的真實程式一樣– 自動載入被自動開啟了,透過bootstrap.php.cache
檔案來完成(這部分的配置預設是在app/phpunit.xml.dist檔案中)
針對指定檔案或目錄的測試也很簡單:
# 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 tests)檢查程式不同層面的整合情況(從路由到視圖)。這些層面本身並不因PHPUnit的介入而改變,只不過它們有著獨特的工作流程:
製造一個請求(request);
-
測試回應(response);
點擊一個鏈接,或提交一個表單;
測試回應;
清除然後重複。
你的第一個功能測試 ¶
#功能測試是存放在Test/AppBundle/Controller
目錄下的普通PHP檔。如果要測試由你的 PostController
處理的頁面,先建立一個新的PostControllerTest.php文件,並繼承一個特殊的 WebTestCase
類別。
作為例程,這個測試看起來可能像下面程式碼:
// 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() ); }}
#為了執行上面的功能測試, WebTestCase
類別啟動了程式核心。在多數情況下,這是自動完成的。但如果kernel位於非標準目錄,你需要調整phpunit.xml.dist
文件,重新設定KERNEL_DIR
環境變數為你的kernel目錄:
<?xml version="1.0" charset="utf-8" ?><phpunit> <php> <server name="KERNEL_DIR" value="/path/to/your/app/" /> </php> <!-- ... --></phpunit>
createClient()
方法回傳了一個client,用它來模擬瀏覽器,使得你能抓取網站頁面:
1 | $crawler = $client->request('GET', '/post/hello-world'); |
$link = $crawler ->filter('a:contains("Greet")') // 选择所有含有"Greet"文本之链接 ->eq(1) // 结果列表中的第二个 ->link() // 点击它; $crawler = $client->click($link);##############
request()方法
(參考更多請求方法)傳回了一個Crawler
對象,用於從回應內容中選擇元素(select elements),完成點擊連結或提交表單這樣的動作。
回應資訊必須是XML或HTML文件格式,抓取器(Crawler)才會運作。若需要不含文件格式的純回應內容(raw content),請使用 $client->getResponse()->getContent()
。
需要點選連結時,先用抓取器選擇它,可使用XPath表達式或CSS拾取器來完成選擇,然後再使用client行為來點選它:
$form = $crawler->selectButton('submit')->form(); // 设置一些值$form['name'] = 'Lucas';$form['form_name[subject]'] = 'Hey there!'; // 提交表单$crawler = $client->submit($form);
提交表單時非常相似:選取表單按鈕,可選擇性地覆寫某些表單項目的值,然後提交對應的表單:
// Assert that the response matches a given CSS selector.// 断言响应内容匹配到一个指定的CSS拾取器$this->assertGreaterThan(0, $crawler->filter('h1')->count());
表單也可以處理上傳,它還包含了用於填寫不同類型的表單欄位的方法(例如select()
和tick()
)。有關細節請參考後面小節 表單(Forms)。
現在你可以很容易地穿梭於程式中,使用斷言來測試頁面是否按你期待的那樣來執行。使用抓取器(Crawler)可令斷言作用於DOM之上。
$this->assertContains( 'Hello World', $client->getResponse()->getContent());
或直接測試回應內容,如果你只是想對內容中是否包含某些文字來斷言的話,又或只需知道回應內容並非XML/HTML格式而已:
use Symfony\Component\HttpFoundation\Response; // ... // Assert that there is at least one h2 tag// with the class "subtitle"// 断言至少有一个h2标签,其class是subtitle$this->assertGreaterThan( 0, $crawler->filter('h2.subtitle')->count()); // Assert that there are exactly 4 h2 tags on the page// 断言页面有4个h2标签$this->assertCount(4, $crawler->filter('h2')); // Assert that the "Content-Type" header is "application/json"// 断言Content-Type头是application/json$this->assertTrue( $client->getResponse()->headers->contains( 'Content-Type', 'application/json' )); // Assert that the response content contains a string// 断言响应信息中包含一个字符串$this->assertContains('foo', $client->getResponse()->getContent());// ...or matches a regex$this->assertRegExp('/foo(bar)?/', $client->getResponse()->getContent()); // Assert that the response status code is 2xx// 断言响应状态码是2xx$this->assertTrue($client->getResponse()->isSuccessful());// Assert that the response status code is 404// 断言响应状态码是404$this->assertTrue($client->getResponse()->isNotFound());// Assert a specific 200 status code// 断言响应状态码是特殊的200$this->assertEquals( 200, // or Symfony\Component\HttpFoundation\Response::HTTP_OK $client->getResponse()->getStatusCode()); // Assert that the response is a redirect to /demo/contact// 断言响应是一个指向/demo/contact的跳转$this->assertTrue( $client->getResponse()->isRedirect('/demo/contact'));// ...or simply check that the response is a redirect to any URL// 或者仅言响应是一个跳转,不管指向的URL是什么$this->assertTrue($client->getResponse()->isRedirect());
使用Test Client ¶
測試客戶端(test client)模擬了一個HTTP客戶端,就像瀏覽器,可以產生針對你Symfony程式的請求。
$crawler = $client->request('GET', '/post/hello-world'); | # request( $method, $uri, array $parameters = array(), array $files = array(), array $server = array(), $content = null, $changeHistory = true) |
request()
方法接受一個HTTP請求方式的參數和一個URL參數,回傳一個Crawler實例。
對於功能測試來說,寫死request連結是最佳實踐。如果在測試中產生URL時使用Symfony路由,它將無法偵測到針對此URL頁面的任何改變,這可能導致使用者體驗受到衝擊。
#使用抓取器來找到回應內容中的DOM元素。這些元素可以用在點擊連結或提交表單等場合:
// 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'));
此處的click()
方法和submit()
方法,都會傳回一個crawler
抓取器物件。這些方法是瀏覽你的程式頁面的最佳方式,因為它們幫你做了很多事,像是從表單中偵測HTTP請求方式,或者給你一個很好的API來上傳文件。
在後面的 Crawler 小節你將學習到更多關於連結連結和表單Form物件的知識。
request
方法也可以用來直接模擬表單提交,或是執行更多複雜請求。一些有用的例子:
1
最後提醒一點,你可以強制讓每次請求在它們各自的PHP進程中被執行,以避免當同一腳本中有多個client時的副作用。
$client->insulate(); | $client->back();$client->forward();$client->reload(); // Clears all cookies and the history// 清除所有cookie和历史记录$client->restart(); |
瀏覽(browsing) ¶
Client物件支援許多真實瀏覽器中的動作:
$history = $client->getHistory();$cookieJar = $client->getCookieJar();
使用內部物件 ¶
getInternalRequest() 和getInternalResponse() 方法從Symfony 2.3起被引入。 如果你使用client來測試你的程序,你可能想要使用client內部的一些物件:
// 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();
你也可以得到與最近一次請求相關的物件:
$container = $client->getContainer();$kernel = $client->getKernel();
如果你的請求沒有處在隔離運行的狀態下,你也可以使用Container
(容器)和Kernel
(核心):
1
使用容器 ¶
強烈建議功能測試只測試回應。但在特定的罕見情況下,你可能要使用內部物件來書寫斷言。這時,你可以呼叫Dependency Injection Container(依賴注入容器,即服務容器):
$container = $client->getContainer(); |
#
// enable the profiler for the very next request// 为接下来的请求开启profiler$client->enableProfiler(); $crawler = $client->request('GET', '/profiler'); // get the profile 得到分析器$profile = $client->getProfile();
#注意,如果你是把client隔離運行或使用一個HTTP layer的話,上述功能無效。程式中的可用服務列表,你可透過 debug:container
命令列工具來查看。在Symfony2.6之前這個指令是
如果你需要檢查的資訊在分析器(profiler)中可用,那麼應使用profiler來取代container。 | 使用分析資料 ¶ |
1#######在測試中使用分析器的某些細節,請參閱###如何在功能測試中使用分析器### 一文。 ######重定向 ###¶#########當一個要求回傳的是重定向回應時,client並不會自動跟進。你可以偵測該回應訊息,然後透過###followRedirect()### 方法來強迫跟進:#######################
$crawler = $client->followRedirect();# ###########
1###############
如果你需要client自動跟進所有重定向,可以強迫它實現,使用followRedirects()
方法:
$client->followRedirects(); | 1 |
將false
傳給followRedirects()
方法,重定向將不再被跟進:
$client->followRedirects(false); | $newCrawler = $crawler->filter('input[type=submit]')
->last()
->parents()
->first(); |
抓取器(Crawler) ¶
#每當你透過client製造一個請求時都有一個抓取器(Crawler)實例被傳回。抓取器允許你遍歷HTML文檔,拾取DOM節點,找到連結和表單。
遍歷(Traversing) ¶
就像JQuery,抓取器有各種方法來遍歷HTML/XML文件的DOM。例如,下述方法可以找到所有input[type=submit]
元素,並選擇頁面中的最後一個,然後選擇它的臨近父元素:
$crawler ->filter('h1') ->reduce(function ($node, $i) { if (!$node->getAttribute('class')) { return false; } }) ->first();
還有很多其他方法可以利用:
filter('h1.title')
- 符合CSS拾取器的所有節點。
filterXpath('h1')
- 符合XPath expression(表達式)的所有節點。
eq(1)
- 指定索引的節點。
first()
- 第一個節點。
last()
- 最後一個節點。
siblings()
- Siblings。
nextAll()
- 後續所有siblings。
previousAll()
- 之前所有siblings。
parents()
- 傳回所有父節點。
children()
- 傳回所有子節點。
reduce($lambda)
- 匿名函數$lambda不傳回false時的所有節點。
由於上述每個方法都返回一個Crawler
抓取器對象,你可以透過對各方法的鍊式呼叫來減少節點拾取過程的程式碼:
// 返回第一个节点的属性值$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');});
可以使用count()
函數來得到儲存在一個Crawler中的節點數量: count($crawler)
#提取資訊 ¶
抓取器可以從節點中提到資訊:
1
連結 ¶
為了選擇鏈接,你可以使用上述遍歷方法,或者使用更方便的selectLink()
快捷方法:
$crawler->selectLink('Click here'); | $link = $crawler->selectLink('Click here')->link(); $client->click($link); |
1##############
如此即可選擇包含了給定文字的所有鏈接,或者是可以點擊的圖片而這些圖片的 alt
屬性中包含了給定文字。和其他的過濾方法類似,它也是回傳一個 Crawler
物件。
一旦你選取了一個連結(link),你就可以使用一個特殊的連結物件( link
object),它是一個專門用來處理連結的方法(類似getMethod()
或getUri()
這種,都屬於幫助方法/helpful method)。要點擊鏈接,使用Client的click()
方法,並把鏈接對象傳進去:
$buttonCrawlerNode = $crawler->selectButton('submit');
表單 ¶
表單可以透過它們的按鈕被選擇,而按鈕可以用selectButton()方法來選擇,正如選擇連結:
##1 | $form = $buttonCrawlerNode->form(); |
selectButton() 方法可以選取
button(按鈕)標籤,以及submit(提交)所在的標籤(即
input 標籤)。它使用按鈕的幾個部分來找到它們:
value
屬性值(The value attribute 值)
- #圖片按鈕的
id
屬性值或
alt屬性值(The id or alt attribute value for images)
button
標籤的
id屬性值或
name屬性值(The id or name attribute value for button tags)
form() 方法即可得到擁有這個button節點的form實例。
$form = $buttonCrawlerNode->form(array( 'name' => 'Fabien', 'my_form[subject]' => 'Symfony rocks!',)); | #1 |
form() 方法時,你也可以把裝有表單欄位值的陣列傳進來,取代預設的:
$form = $buttonCrawlerNode->form(array(), 'DELETE');當你要模擬一個特定的HTTP表單提交方式時,把它傳到第二個參數:
1 | $client->submit($form); |
# $client->submit($form, array( 'name' => 'Fabien', 'my_form[subject]' => 'Symfony rocks!',)); | // 改变某个字段的值/Change the value of a field$form['name'] = 'Fabien';$form['my_form[subject]'] = 'Symfony rocks!'; |
欄位的值可以傳到submit()方法的第二個參數:
// 选取一个option单选或radio单选$form['country']->select('France'); // 选取一个checkbox$form['like_symfony']->tick(); // 上传一个文件$form['photo']->upload('/path/to/lucas.jpg');
對於更複雜的情況,可以把表單實例當作數組,來分別設置每一個欄位對應的值:
// 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());
還有一個很好用的API,可根據欄位類型來管理它們的值:
// 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());
如果你有目的地想要選擇「無效的」select/radio值,參考選擇無效的Choice Values。
你可以得到即將被提交的表單欄位值,呼叫Form物件的 getValues()
方法即可。已上傳的檔案也可以從一個分離出來的陣列得到,這個陣列由 getFiles()
方法傳回。 getPhpValues()
以及getPhpFiles()
方法同樣可以返回已經提交的字段,只不過是PHP格式的(它把帶有方括號的鍵– 比如my_form[ subject]
– 給轉換成PHP陣列)。
新增或刪除表單到Collection ¶
#如果你使用了Collection of Forms(表單集合),你不能對既有表單增加一個$form['task[tags][0][name]'] = 'foo
欄位。這會導致一個 Unreachable field "…"
錯誤。因為 $form
只能被用來設定現有的欄位。為了新增一個字段,你不得不把值加到一個原始數組中:
PHP:// app/config/config_test.php // ...$container->loadFromExtension('swiftmailer', array(
'disable_delivery' => true,));
這裡的task[tags][0][name]
就是由JavaScript創建的字段的名字。
你可以移除一個既有字段,例如一個標籤(tag):
XML:<!-- app/config/config_test.xml --><?xml version="1.0" encoding="UTF-8" ?><container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> <!-- ... -->
<swiftmailer:config disable-delivery="true" /></container>
設定測試 ¶
功能測試所用到的Client創建了一個Kernel,用於在一個特殊的test
測試環境中運行。由於Symfony在測試時載入的是 app/config/config_test.yml
,你可以調整任何一項用於測試的「程式層級」設定。
例如,預設的Swift Mailer被設定為不要在測試環境發送郵件。設定檔中的相關選項是這樣設定的:
YAML:# app/config/config_test.yml # ...swiftmailer:
disable_delivery: true
$client = static::createClient(array(
'environment' => 'my_test_env',
'debug' => false,));
$client = static::createClient(array(), array( 'HTTP_HOST' => 'en.example.com', 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',));
你也可以使用一個完全不同的環境,或是覆寫預設的debug模式( true
),透過把相關選項傳給 createClient()
方法即可:
$client->request('GET', '/', array(), array(), array(
'HTTP_HOST' => 'en.example.com',
'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',));
如果你的程式需要經過一些HTTP頭才能運轉,把它們傳給createClient()方法的第二個參數:
<!-- app/phpunit.xml.dist --><phpunit>
<!-- ... -->
<testsuites>
<testsuite name="Project Test Suite">
<directory>../src/*/*Bundle/Tests</directory>
<directory>../src/*/Bundle/*Bundle/Tests</directory>
<directory>../src/*Bundle/Tests</directory>
</testsuite>
</testsuites>
<!-- ... --></phpunit>
你也可以在每一個request基本程序中覆寫HTTP header:
<!-- app/phpunit.xml.dist --><phpunit>
<!-- ... -->
<testsuites>
<testsuite name="Project Test Suite">
<!-- ... - - -> <directory>../lib/tests</directory> </testsuite> </testsuites> <!-- ... - - -></phpunit>
client在test
測試環境下是容器中的可用服務(或無論什麼環境,只要framework.test選項被開啟)。這意味著只要需要,你可以覆寫整個client服務。
PHPUnit配置 ¶
#每一個程序,有它自己的PHPUnit配置,就存在app/phpunit. xml.dist
檔案中。你可以編輯這個文件,改變預設值,或建立一個僅供你本地機器設定測試選項的 app/phpunit.xml
文件。
將 app/phpunit.xml.dist
檔案存在你的程式碼寶庫中並且忽略 app/phpunit.xml
檔案。
預設是只有存放在標準目錄(src//Bundle/Tests, src//Bundle/Bundle/Tests, src/ *Bundle/Tests)下面你的自訂bundle中的測試才能夠被phpunit指令執行。這在app/phpunit.xml.dist檔案中已經配置好了:
<!-- app/phpunit.xml.dist --><phpunit>
<!-- ... -->
<filter>
<whitelist>
<!-- ... -->
<directory>../lib</directory>
<exclude>
<!-- ... -->
<directory>../lib/tests</directory>
</exclude>
</whitelist>
</filter>
<!-- ... - - -></phpunit>
但是你也可以很容易地加入更多目錄。例如,下列配置新增了自訂目錄lib/tests中的測試:
為了在程式碼覆蓋範圍中包容其他目錄,還需添加