PHPUnit + Selenium = Code Coverage Report
โดยปกติ PHPUnit ซึ่งใช้ทำ Unit test นั้น จะไม่สามารถทำการทดสอบผ่านเว็บบราวเซอร์ตรงๆได้ ดังนั้น ถ้าหากใช้รูปแบบการพัฒนาโปรแกรมแบบ MVC อาจจะทำการทดสอบได้เฉพาะ Model เท่านั้น หากจะทดสอบ View / Control ต้องใช้เครื่องมือคือ Selenium ในการทำการทดสอบผ่านหน้าเว็บบราวเซอร์
ดังนั้น ถ้าเราต้องการสร้าง Code Coverage Report ขึ้นมา จะต้องทำผ่าน Selenium Interface ด้วย. ผมได้เขียน Guideline ในการสร้าง Code Coverage Report กับ Yii Framework ไว้ที่ Yii Framework Forum ครับ รายละเอียดสามารถตามอ่านได้ตามลิงค์
เครื่องมือที่ผมใช้พัฒนา PHP ประกอบด้วย
- Apache 2
- Windows 7
- Netbeans 6.9.1 พร้อมกับ Selenium Plugin
- PHP 5.3 กับ XDebug module
- PEAR/PHPUnit
จริงๆ แล้วใน Netbeans website มี Tutorial อยู่เช่นกัน แต่ว่า ในส่วนนี้ ผมจะแนะนำสำหรับคนที่ทำเวอร์ชวลโฮส ที่มีเว็บไซต์หลายๆเว็บไซต์ภายในเครื่อง. แนะนำให้ทำเฉพาะเครื่องสำหรับ Development เท่านั้น สำหรับ Production server จะทำให้ช้าลงมาก ไม่แนะนำ
ก่อนอื่น ทำการแก้ไขไฟล์ ดังรายละเอียดด้านล่างนี้
File: %PEAR_Directory%/PHPUnit/Extensions/SeleniumTestCase/prepend.php
// $GLOBALS['PHPUNIT_COVERAGE_DATA_DIRECTORY'] = FALSE; $GLOBALS['PHPUNIT_COVERAGE_DATA_DIRECTORY'] = $_SERVER["DOCUMENT_ROOT"] ;
File: %PEAR_Directory%/PHPUnit/Extensions/SeleniumTestCase/phpunit_coverage.php
//$GLOBALS['PHPUNIT_COVERAGE_DATA_DIRECTORY'] = getcwd(); $GLOBALS['PHPUNIT_COVERAGE_DATA_DIRECTORY'] = $_SERVER["DOCUMENT_ROOT"] ;
File: php.ini (ไฟล์ Configuration ของ PHP)
; Automatically add files before PHP document. auto_prepend_file = "D:\Service\var\scripts\PHPUnit\Extensions\SeleniumTestCase\prepend.php" ; Automatically add files after PHP document. auto_append_file = "D:\Service\var\scripts\PHPUnit\Extensions\SeleniumTestCase\append.php"
File: httpd.conf (ไฟล์ Configuration ของ Apache2)
<Directory "D:/Service/var/scripts"> Options FollowSymLinks Indexes Allow from all Order allow,deny AllowOverride All </Directory>
ขั้นตอนต่อไป เป็นขั้นตอนสำหรับทำให้ไดเรกทอรี่ดังกล่าว สามารถใช้ได้ในเว็บไซต์ใดๆ ที่อยู่ในเครื่อง
File: httpd.conf (ไฟล์ Configuration ของ Apache2)
# LoadModule alias_module modules/mod_alias.so LoadModule alias_module modules/mod_alias.so<IfModule alias_module> # ScriptAlias /cgi-bin/ "E:/Service/Process/Apache2.2/cgi-bin/" Alias /selenium-phpunit/ "D:/Service/var/scripts/PHPUnit/Extensions/SeleniumTestCase/" </IfModule>
ต่อมาขั้นตอนสำคัญ อย่าลืม Restart Apache 2 service
ต่อจากนี้เป็นขั้นตอนเพิ่มเติมสำหรับ Yii Framework
File: %YiiFramework%/protected/test/WebTest.php
// Add this to class protected $coverageScriptUrl = 'http://your.localhost./selenium-phpunit/phpunit_coverage.php';
จากนั้นอย่าลืมตรวจสอบไฟล์ phpunit.xml ให้ทำการสร้าง Code Coverage report เมื่อมีการทำการทดสอบ
File: %YiiFramework%/protected/test/phpunit.xml
<logging> <log type="coverage-html" target="%YiiFramework%\protected\tests\coverage" charset="UTF-8" yui="true" highlight="true" lowUpperBound="35" highLowerBound="70" /> </logging>
เสร็จครับ หมดแล้ว หลังจากนั้น เริ่มใช้งานได้เลย
การเขียน Pre-step เงื่อนไขก่อนทำเทสเคส
การใช้ PHPUnit ร่วมกับ Selenium ปกติแล้ว เราจะมี 2 Methods ที่ช่วยในการทำงานก่อนและหลังทำเทสเคส นั่นคือ setUp(); และ tearDown();
ฟังก์ชั่น setUp(); จะทำงานทุกครั้งก่อนที่จะทำเทสเคสใดๆ และเมื่อทำเทสเคสเสร็จเรียบร้อยแล้วจะเรียก tearDown(); ฟังก์ชั่นมาปิดท้ายทุกครั้ง ดังนั้น จึงเป็นประโยชน์กรณีที่เราต้องเตรียมตัวอย่างสำหรับทำการทดสอบ หรือการทำลายตัวแปร หรือข้อมูล ภายหลังจากการทำการทดสอบเสร็จสิ้น
ในการใช้ PHPUnit ร่วมกับ Selenium เพื่อทดสอบเว็บแอพลิเคชั่นนั้น เวลาจะสั่งให้เว็บบราวเซอร์ทำการทดสอบใดๆ ตัว Selenium จะต้องทำการ Initialize สร้าง SessionID ขึ้นมาก่อน ดังนั้น ถ้าหากเราเขียนเทสเคสดังข้างล่าง จะไม่เกิดปัญหา เพราะหลังมันเรียกฟังก์ชั่น setUp(); แล้ว ภายในเฟรมเวิร์คจะมีไปเรียกฟังก์ชั่นอื่นๆ อีกนิดหน่อย เพื่อทำการ Initialize ค่าของ Browser ก่อนที่จะเข้าไปทำในแต่ละเทสเคส
protected function setUp()
{
parent::setUp();
}
protected function tearDown() {}
public function testActionIndex() {
// Login
$this->open("/");
$this->click("link=Login");
$this->waitForPageToLoad("30000");
$this->type("LoginForm_username", "username");
$this->type("LoginForm_password", "password");
$this->click("yt0");
$this->waitForPageToLoad("30000");
// Test Case
$this->click("link=Send SMS");
$this->waitForPageToLoad("30000");
$this->type("SendSMSForm_to", "0123456789");
$this->type("SendSMSForm_message", "Message Test");
$this->click("yt0");
$this->waitForPageToLoad("30000");
try {
$this->assertTrue($this->isTextPresent("ส่งเรียบร้อย"));
} catch (PHPUnit_Framework_AssertionFailedError $e) {
array_push($this->verificationErrors, $e->toString());
}
// Logout
$this->click("link=Logout (demo)");
$this->waitForPageToLoad("30000");
}
}
ปัญหามันเกิดขึ้น ตรงที่ว่า ตอน Initialize browser มันจะทำตอนจบ setUp(); method ไปแล้ว และทำก่อนเริ่มเทสเคส ดังนั้น ถ้าเรามี Pre-step อย่างจากตัวอย่าง Source code ด้านบน ถ้าเราใส่ Login ซึ่งเป็น Pre-step ไปในส่วน setUp(); จะเกิดปัญหา เพราะว่า Browser ยังไม่ถูก Initialize และจะแจ้งเป็น Error Message ดังข้างล่างนี้
PHPUnit_Framework_Exception: Response from Selenium RC server for click(link=Login).ERROR Server Exception: sessionId should not be null; has this session been started yet?.
ปัญหานี้ จะเกิดขึ้นเมื่อเราทำการ Login ไปโดยไม่ได้ Initialize Browser ขึ้นมาก่อน ดังข้างล่าง
parent::setUp();
$this->setBrowserUrl(TEST_BASE_URL);
$this->click("link=Login");
$this->waitForPageToLoad("30000");
$this->type("LoginForm_username", "username");
$this->type("LoginForm_password", "password");
$this->click("yt0");
$this->waitForPageToLoad("30000");
$this->click("link=Send SMS");
}
วิธีการคือ เราต้องสั่งให้ Selenium ทำงานก่อน โดยใช้ start(); method และตั้งค่าให้ไม่ต้องหยุดการทำงานอัตโนมัติเมื่อทำเทสเคสเสร็จ ผ่านฟังก์ชั่น setAutoStop(false);
โดยเราจะทำการหยุดการทำงานเอง ผ่าน tearDown(); ท้ายที่สุดแล้ว จึงได้ ซอสโค้ดประมาณนี้
- class AppControllerTest extends WebTestCase {
- protected function setUp() {
- parent::setUp();
- $this->setBrowser("*googlechrome");
- $this->setBrowserUrl(TEST_BASE_URL);
- $this->setAutoStop(false);
- $this->start();
- $this->open("/index-test.php");
- $this->click("link=Login");
- $this->waitForPageToLoad("30000");
- $this->type("LoginForm_username", "username");
- $this->type("LoginForm_password", "password");
- $this->click("yt0");
- $this->waitForPageToLoad("30000");
- $this->click("link=Send SMS");
- }
- protected function tearDown() {
- $this->stop();
- }
- public function testActionIndex() {
- $this->waitForPageToLoad("30000");
- $this->type("SendSMSForm_to", "0123456789");
- $this->type("SendSMSForm_message", "Message Test");
- $this->click("yt0");
- $this->waitForPageToLoad("30000");
- try {
- $this->assertTrue($this->isTextPresent("ส่งเรียบร้อย"));
- } catch (PHPUnit_Framework_AssertionFailedError $e) {
- }
- $this->click("link=Logout (demo)");
- $this->waitForPageToLoad("30000");
- }
- }
บรรทัดที่ 11, 13 เป็นการสั่งให้ Initialize Browser ก่อนที่เราจะใส่ Pre-step ในบรรทัดต่อๆ มาครับ.
การกำหนด Selenium ให้ใช้ Google Chrome ในการทดสอบ
โดยปกติแล้ว PHPUnit เมื่อใช้งานร่วมกับ Selenium แล้ว บราวเซอร์ที่ใช้ในการทดสอบอัตโนมัติจะเป็น Firefox ดังนั้น ถ้าเราต้องการจะเปลี่ยนบราวเซอร์ที่ใช้ในการทดสอบ สามารถจัดการได้ ดังนี้
วิธีที่ 1 แก้ไขไฟล์ phpunit.xml ที่ใช้เป็น configuration เวลารัน phpunit
<browser name="Google Chrome" browser="*googlechrome" />
</selenium>
วิธีที่ 2 กำหนดในซอสโค้ดที่ต้องการทำการรัน
$this->setBrowser("*googlechrome");
ต้องอย่าลืมว่า ถ้าหากมีการกำหนดใน XML Configuration แล้วก็ตาม ให้มาดูในซอสโค้ดด้วยว่าได้กำหนดเป็นอย่างอื่นหรือไม่ ถ้าหากยังรันผิดบราวเซอร์อยู่
ข้อผิดพลาด
ข้อผิดพลาดที่อาจจะพบได้คือ การกำหนด Attribute ของ browser เป็น *chrome ไม่ใช่ *googlechrome ซึ่ง *chrome จะทำงานในอีกโหมดหนึ่งของ Firefox และที่สำคัญคืออย่าลืมเครื่องหมายดาว '*' ข้างหน้าชื่อบราวเซอร์ด้วย


