不管什麼時候,你只要編寫一行新的代碼,你就有可能引入新的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。