การพัฒนาที่ขับเคลื่อนด้วยการทดสอบและการทดสอบหน่วยเป็นวิธีล่าสุดเพื่อให้แน่ใจว่าโค้ดยังคงทำงานตามที่คาดไว้ แม้ว่าจะมีการแก้ไขและปรับแต่งที่สำคัญก็ตาม ในบทความนี้ คุณจะได้เรียนรู้วิธีหน่วยทดสอบโค้ด PHP ของคุณที่เลเยอร์โมดูล ฐานข้อมูล และอินเทอร์เฟซผู้ใช้ (UI)
เวลา 3 โมงเช้า เราจะรู้ได้อย่างไรว่าโค้ดของเรายังใช้งานได้
อยู่ เว็บแอปพลิเคชันทำงานตลอด 24 ชั่วโมงทุกวัน ดังนั้นคำถามที่ว่าโปรแกรมของฉันยังทำงานอยู่หรือไม่จะรบกวนฉันในเวลากลางคืน การทดสอบหน่วยช่วยให้ฉันสร้างความมั่นใจในโค้ดได้เพียงพอจนฉันสามารถนอนหลับได้ดี
การทดสอบหน่วยเป็นเฟรมเวิร์กสำหรับการเขียนกรณีทดสอบสำหรับโค้ดของคุณและเรียกใช้การทดสอบเหล่านี้โดยอัตโนมัติ การพัฒนาที่ขับเคลื่อนด้วยการทดสอบเป็นแนวทางการทดสอบหน่วยตามแนวคิดที่ว่าคุณควรเขียนการทดสอบก่อนและตรวจสอบว่าการทดสอบเหล่านี้สามารถค้นหาข้อผิดพลาดได้ จากนั้นจึงเริ่มเขียนโค้ดที่ต้องผ่านการทดสอบเหล่านี้เท่านั้น เมื่อผ่านการทดสอบทั้งหมด คุณลักษณะที่เราพัฒนาขึ้นก็เสร็จสมบูรณ์ ค่าของการทดสอบหน่วยเหล่านี้คือเราสามารถเรียกใช้การทดสอบเหล่านี้ได้ตลอดเวลา ก่อนที่จะตรวจสอบในโค้ด หลังจากการเปลี่ยนแปลงครั้งใหญ่ หรือหลังจากการปรับใช้กับระบบที่ทำงานอยู่
การทดสอบหน่วย PHP
สำหรับ PHP กรอบการทดสอบหน่วยคือ PHPUnit2 ระบบนี้สามารถติดตั้งเป็นโมดูล PEAR ได้โดยใช้บรรทัดคำสั่ง PEAR: % pear ติดตั้ง PHPUnit2
หลังจากติดตั้งเฟรมเวิร์ก คุณสามารถเขียนการทดสอบหน่วยได้โดยการสร้างคลาสทดสอบที่ได้รับจาก PHPUnit2_Framework_TestCase
การทดสอบหน่วยโมดูล
ฉันพบว่าสถานที่ที่ดีที่สุดในการเริ่มต้นการทดสอบหน่วยอยู่ในโมดูลตรรกะทางธุรกิจของแอปพลิเคชัน ฉันใช้ตัวอย่างง่ายๆ: นี่คือฟังก์ชันที่รวมตัวเลขสองตัว เพื่อเริ่มการทดสอบ ขั้นแรกเราจะเขียนกรณีทดสอบตามที่แสดงด้านล่าง
รายการ 1. TestAdd.php
<?phprequire_once 'Add.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAdd ขยาย PHPUnit2_Framework_TestCase{ function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); } function test2() { $this->assertTrue( add( 1, 1 ) == 2 ); }}?>
คลาส TestAdd นี้มีสองวิธี ซึ่งทั้งสองวิธีใช้คำนำหน้าการทดสอบ แต่ละวิธีจะกำหนดการทดสอบ ซึ่งอาจง่ายเหมือนรายการ 1 หรือซับซ้อนมาก ในกรณีนี้ เราเพียงยืนยันว่า 1 บวก 2 เท่ากับ 3 ในการทดสอบครั้งแรก และ 1 บวก 1 เท่ากับ 2 ในการทดสอบครั้งที่สอง
ระบบ PHPUnit2 กำหนดเมธอด assertTrue() ซึ่งใช้เพื่อทดสอบว่าค่าเงื่อนไขที่มีอยู่ในพารามิเตอร์เป็นจริงหรือไม่ จากนั้นเราจึงเขียนโมดูล Add.php ซึ่งเริ่มแรกให้ผลลัพธ์ที่ไม่ถูกต้อง
รายการ 2. Add.php
<?phpfunction add( $a, $b ) { return 0; }?>
ตอนนี้เมื่อรันการทดสอบหน่วย การทดสอบทั้งสองล้มเหลว
รายการ 3. การทดสอบความล้มเหลว
% phpunit TestAdd.phpPHPUnit 2.2.1 โดย Sebastian Bergmann.FFTime: 0.0031270980834961 มี 2 ความล้มเหลว: 1) test1 (TestAdd) 2) test2 (TestAdd) ความล้มเหลว !!! การทดสอบทำงาน: 2, ความล้มเหลว: 2, ข้อผิดพลาด: 0 การทดสอบที่ไม่สมบูรณ์: 0
ตอนนี้ฉันรู้ว่าการทดสอบทั้งสองทำงานได้ดี ดังนั้นฟังก์ชัน add() จึงสามารถแก้ไขได้เพื่อให้ทำงานจริงได้
ตอนนี้การทดสอบทั้งสองผ่านแล้ว
<?phpfunction add( $a, $b ) { return $a+$b; }?>
รายการ 4. การทดสอบที่ผ่าน
% phpunit TestAdd.phpPHPUnit 2.2.1 โดย Sebastian Bergmann...เวลา: 0.0023679733276367OK (2 การทดสอบ)%
แม้ว่า ตัวอย่างของการพัฒนาที่ขับเคลื่อนด้วยการทดสอบนี้เรียบง่ายมาก แต่เราสามารถเข้าใจแนวคิดของมันได้ ขั้นแรกเราสร้างกรณีทดสอบและมีโค้ดเพียงพอที่จะทำการทดสอบ แต่ผลลัพธ์กลับไม่ถูกต้อง จากนั้นเราจะตรวจสอบว่าการทดสอบล้มเหลวจริงๆ จากนั้นจึงนำโค้ดจริงไปใช้เพื่อให้การทดสอบผ่าน
ฉันพบว่าในขณะที่ฉันใช้โค้ด ฉันจะเพิ่มโค้ดต่อไปจนกว่าฉันจะมีการทดสอบที่สมบูรณ์ซึ่งครอบคลุมเส้นทางโค้ดทั้งหมด ในตอนท้ายของบทความนี้ คุณจะพบคำแนะนำเกี่ยวกับการทดสอบที่ควรเขียนและวิธีเขียน
การทดสอบฐานข้อมูล
หลังจากการทดสอบโมดูล การทดสอบการเข้าถึงฐานข้อมูลสามารถทำได้ การทดสอบการเข้าถึงฐานข้อมูลทำให้เกิดประเด็นที่น่าสนใจสองประเด็น ขั้นแรก เราต้องคืนค่าฐานข้อมูลไปยังจุดที่ทราบก่อนการทดสอบแต่ละครั้ง ประการที่สอง โปรดทราบว่าการกู้คืนนี้อาจทำให้เกิดความเสียหายต่อฐานข้อมูลที่มีอยู่ ดังนั้นเราจึงต้องทดสอบกับฐานข้อมูลที่ไม่ได้ใช้งานจริง หรือระวังไม่ให้ส่งผลกระทบต่อเนื้อหาของฐานข้อมูลที่มีอยู่เมื่อเขียนกรณีทดสอบ
การทดสอบหน่วยฐานข้อมูลเริ่มต้นจากฐานข้อมูล เพื่ออธิบายปัญหานี้ เราจำเป็นต้องใช้รูปแบบง่ายๆ ต่อไปนี้
รายการ 5. Schema.sql
DROP TABLE หากมีผู้เขียนอยู่; สร้างผู้เขียนตาราง (id MEDIUMINT NOT NULL AUTO_INCREMENT, ชื่อ TEXT NOT NULL, PRIMARY KEY (id));
รายการ 5 เป็นตารางผู้เขียน และแต่ละระเบียนมี ID ที่เกี่ยวข้อง
ต่อไปคุณสามารถเขียนกรณีทดสอบได้
รายการ 6. TestAuthors.php
<?phprequire_once 'dblib.php';require_once 'PHPUnit2/Framework/TestCase.php';คลาส TestAuthors ขยาย PHPUnit2_Framework_TestCase{ function test_delete_all() { $this->assertTrue( Authors::delete_all() ); } function test_insert() { $this->assertTrue( Authors::delete_all() ); $this->assertTrue( Authors::insert( 'Jack' ) ); } ฟังก์ชัน test_insert_and_get() { $this->assertTrue( Authors ::delete_all() ); $this->assertTrue( ผู้แต่ง::insert( 'Jack' ) ); $this->assertTrue( ผู้แต่ง::insert( 'Joe' ) ); ; $this->assertTrue( $found != null ); $this->assertTrue( count( $found ) == 2 ); }}?>
ชุดการทดสอบนี้ครอบคลุมถึงการลบผู้เขียนออกจากตารางและการแทรกผู้เขียนลงในตาราง พร้อมทั้งฟังก์ชั่นต่างๆ เช่น การแทรกผู้เขียนในขณะที่ตรวจสอบว่ามีผู้เขียนอยู่หรือไม่ นี่เป็นการทดสอบแบบสะสมซึ่งฉันพบว่ามีประโยชน์มากในการค้นหาจุดบกพร่อง ด้วยการสังเกตว่าการทดสอบใดได้ผลและการทดสอบใดไม่ได้ผล คุณจะสามารถทราบได้อย่างรวดเร็วว่ามีอะไรผิดปกติ จากนั้นจึงเข้าใจความแตกต่างเพิ่มเติม
เวอร์ชันของรหัสการเข้าถึงฐานข้อมูล dblib.php PHP ที่แต่เดิมทำให้เกิดความล้มเหลวแสดงไว้ด้านล่าง
รายการ 7. dblib.php
<?phprequire_once('DB.php'); class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; :Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage()); } ส่งคืน $db; } ฟังก์ชันคงที่สาธารณะ Delete_all() { return false; } public static function insert( $name ) { return false; } public static function get_all() { return null; }}?>
การดำเนินการทดสอบหน่วยกับโค้ดในรายการ 8 จะแสดงว่าการทดสอบทั้งสามรายการล้มเหลว:
รายการ 8. dblib. php
% phpunit TestAuthors.phpPHPUnit 2.2.1 โดย Sebastian Bergmann.FFFTime: 0.007500171661377 มีความล้มเหลว 3 ครั้ง: 1) test_delete_all (TestAuthors) 2) test_insert (TestAuthors) 3) test_insert_and_get (TestAuthors) ความล้มเหลว !!! การทดสอบทำงาน: 3, ส: 3 ข้อผิดพลาด: 0 การทดสอบที่ไม่สมบูรณ์: 0.%
ตอนนี้เราสามารถเริ่มเพิ่มโค้ดเพื่อเข้าถึงฐานข้อมูลได้อย่างถูกต้องทีละวิธี จนกว่าการทดสอบทั้ง 3 รายการจะผ่าน โค้ด dblib.php เวอร์ชันสุดท้ายแสดงอยู่ด้านล่าง
รายการ 9. ทำ dblib.php
<?phprequire_once('DB.php');class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; $db =& DB ::Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage()); } ส่งคืนฟังก์ชันคงที่สาธารณะ $db = ผู้เขียน::get_db(); $sth = $db->prepare( 'DELETE FROM authors' ); $db->execute( $sth ); } public static function insert( $name ) { $db = ผู้แต่ง::get_db(); $sth = $db->prepare( 'INSERT INTO authors VALUES (null,?)' ); $db->execute( $sth, array( $name ) ); ฟังก์ชันคงที่ get_all() { $db = Authors::get_db(); $res = $db->query( "SELECT * FROM authors" ); $rows = array(); ) ) { $rows []= $row; } return $rows; }}?>
การทดสอบ HTML
ขั้นตอนต่อไปในการทดสอบแอปพลิเคชัน PHP ทั้งหมดคือการทดสอบอินเทอร์เฟซ Hypertext Markup Language (HTML) ส่วนหน้า เพื่อทำการทดสอบนี้ เราจำเป็นต้องมีเว็บเพจแบบด้านล่างนี้
รายการ 10. TestPage.php
<?phprequire_once 'HTTP/Client.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestPage ขยาย PHPUnit2_Framework_TestCase{ function get_page( $url ) { $client = new HTTP_Client(); $client ->get( $url ); $resp = $client->currentResponse(); return $resp['body']; /add.php' ); $this->assertTrue( strlen( $page ) > 0 ); $this->assertTrue( preg_match( '/<html>/', $page ) == 1 ); ) { $page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' ); $this->assertTrue( strlen( $page ) > 0 ); assertTrue( preg_match( '/<html>/', $page ) == 1 ); preg_match( '/<span id="result">(.*?)</span>/', $หน้า, $ออก ); $this->assertTrue( $out[1]=='30' ); }}?>
การทดสอบนี้ใช้โมดูลไคลเอ็นต์ HTTP ที่ PEAR จัดเตรียมไว้ ฉันพบว่ามันง่ายกว่า PHP Client URL Library (CURL) ในตัวเล็กน้อย แต่อันหลังก็สามารถใช้ได้เช่นกัน
มีการทดสอบที่จะตรวจสอบหน้าที่ส่งคืนและพิจารณาว่ามี HTML หรือไม่ การทดสอบครั้งที่สองขอผลรวมของ 10 และ 20 โดยการวางค่าใน URL ที่ร้องขอ จากนั้นตรวจสอบผลลัพธ์ในหน้าที่ส่งคืน
รหัสสำหรับหน้านี้แสดงอยู่ด้านล่าง
รายการ 11. TestPage.php
<html><body><form><input type="text" name="a" value="<?php echo($_REQUEST['a']); ?>" /> + <input type="text" name="b" value="<?php echo($_REQUEST['b']); ?>" /> =<span id="result"><?php echo($_REQUEST) ['a']+$_REQUEST['b']); ?></span><br/><input type="submit" value="Add" /></form></body></html >
หน้านี้ค่อนข้างง่าย ช่องป้อนข้อมูลสองช่องแสดงค่าปัจจุบันที่ให้ไว้ในคำขอ ช่วงผลลัพธ์จะแสดงผลรวมของค่าทั้งสองนี้ มาร์กอัปทำเครื่องหมายความแตกต่างทั้งหมด: ผู้ใช้จะมองไม่เห็น แต่การทดสอบหน่วยจะมองเห็นได้ ดังนั้นการทดสอบหน่วยจึงไม่จำเป็นต้องมีตรรกะที่ซับซ้อนในการค้นหาค่านี้ แต่จะดึงค่าของแท็กเฉพาะแทน ด้วยวิธีนี้ เมื่ออินเทอร์เฟซเปลี่ยนแปลง ตราบใดที่สแปนมีอยู่ การทดสอบก็จะผ่านไป
เช่นเคย ให้เขียนกรณีทดสอบก่อน จากนั้นจึงสร้างเวอร์ชันที่ล้มเหลวของเพจ เราทดสอบความล้มเหลวแล้วแก้ไขเนื้อหาของเพจเพื่อให้ทำงานได้ ผลลัพธ์มีดังนี้:
รายการ 12. การทดสอบล้มเหลวแล้วแก้ไขหน้า
% phpunit TestPage.phpPHPUnit 2.2.1 โดย Sebastian Bergmann...เวลา: 0.25711488723755OK (2 การทดสอบ)%
การทดสอบทั้งสองสามารถผ่านได้ ซึ่งหมายความว่าการทดสอบ รหัสมันทำงานได้ดี
เมื่อรันการทดสอบโค้ดนี้ การทดสอบทั้งหมดจะทำงานโดยไม่มีปัญหา ดังนั้นเราจึงรู้ว่าโค้ดของเราทำงานได้อย่างถูกต้อง
แต่การทดสอบส่วนหน้า HTML มีข้อบกพร่อง: JavaScript รหัสไคลเอ็นต์ Hypertext Transfer Protocol (HTTP) ดึงข้อมูลเพจ แต่ไม่ได้รัน JavaScript ดังนั้นหากเรามีโค้ดจำนวนมากใน JavaScript เราจะต้องสร้างการทดสอบหน่วยระดับตัวแทนผู้ใช้ วิธีที่ดีที่สุดที่ฉันค้นพบเพื่อให้บรรลุฟังก์ชันนี้คือการใช้ฟังก์ชันเลเยอร์อัตโนมัติที่มีอยู่ใน Microsoft® Internet Explorer® ด้วยสคริปต์ Microsoft Windows® ที่เขียนด้วย PHP คุณสามารถใช้อินเทอร์เฟซ Component Object Model (COM) เพื่อควบคุม Internet Explorer เพื่อนำทางระหว่างเพจต่างๆ จากนั้นใช้วิธี Document Object Model (DOM) เพื่อค้นหาเพจหลังจากดำเนินการกับองค์ประกอบเฉพาะของผู้ใช้ .
นี่เป็นวิธีเดียวที่ฉันรู้ในการทดสอบโค้ด JavaScript ส่วนหน้า ฉันยอมรับว่าการเขียนและบำรุงรักษาไม่ใช่เรื่องง่าย และการทดสอบเหล่านี้เสียหายได้ง่ายเมื่อมีการเปลี่ยนแปลงแม้แต่เล็กน้อยในหน้าเว็บ
การทดสอบใดที่จะเขียนและวิธีการเขียน
เมื่อเขียนการทดสอบ ฉันชอบที่จะครอบคลุมสถานการณ์ต่อไปนี้:
การทดสอบเชิงบวกทั้งหมด
ชุดการทดสอบนี้ช่วยให้แน่ใจว่าทุกอย่างทำงานได้ตามที่เราคาดหวัง
การทดสอบเชิงลบทั้งหมด
ใช้การทดสอบเหล่านี้ทีละรายการเพื่อให้แน่ใจว่ามีการทดสอบความล้มเหลวหรือความผิดปกติทุกครั้ง
การทดสอบลำดับเชิงบวก
ชุดการทดสอบนี้ช่วยให้แน่ใจว่าการเรียกในลำดับที่ถูกต้องทำงานได้ตามที่เราคาดหวัง
การทดสอบลำดับเชิงลบ
ชุดการทดสอบนี้ช่วยให้แน่ใจว่าการโทรล้มเหลวเมื่อไม่ได้ทำในลำดับที่ถูกต้อง
การทดสอบโหลด
ในกรณีที่เหมาะสม สามารถทำการทดสอบชุดเล็กๆ เพื่อพิจารณาว่าประสิทธิภาพของการทดสอบเหล่านี้อยู่ภายในความคาดหวังของเรา เช่น การโทร 2,000 ครั้งควรจะเสร็จสิ้นภายใน 2 วินาที
การทดสอบทรัพยากร
การทดสอบเหล่านี้ช่วยให้แน่ใจว่า Application Programming Interface (API) จัดสรรและปล่อยทรัพยากรอย่างถูกต้อง เช่น การเรียก API แบบเปิด เขียน และปิดหลายครั้งติดต่อกันเพื่อให้แน่ใจว่าไม่มีไฟล์ใดยังคงเปิดอยู่
การทดสอบการเรียกกลับ
สำหรับ API ที่มีวิธีการเรียกกลับ การทดสอบเหล่านี้ทำให้มั่นใจได้ว่าโค้ดจะทำงานตามปกติหากไม่มีการกำหนดฟังก์ชันการเรียกกลับ นอกจากนี้ การทดสอบเหล่านี้ยังสามารถรับประกันได้ว่าโค้ดยังคงสามารถทำงานได้ตามปกติเมื่อมีการกำหนดฟังก์ชันการเรียกกลับ แต่ฟังก์ชันการเรียกกลับเหล่านี้ทำงานไม่ถูกต้องหรือสร้างข้อยกเว้น
ต่อไปนี้เป็นแนวคิดบางประการเกี่ยวกับการทดสอบหน่วย ฉันมีข้อเสนอแนะบางประการเกี่ยวกับวิธีเขียนการทดสอบหน่วย:
อย่าใช้ข้อมูลแบบสุ่ม
แม้ว่าการสร้างข้อมูลแบบสุ่มในอินเทอร์เฟซอาจดูเหมือนเป็นความคิดที่ดี แต่เราต้องการหลีกเลี่ยงการทำเช่นนี้เนื่องจากข้อมูลนี้อาจกลายเป็นเรื่องยากมากในการแก้ไข หากข้อมูลถูกสร้างขึ้นแบบสุ่มในการโทรแต่ละครั้ง อาจเกิดข้อผิดพลาดในการทดสอบครั้งหนึ่ง แต่ไม่ใช่ในการทดสอบอื่น หากการทดสอบของคุณต้องการข้อมูลแบบสุ่ม คุณสามารถสร้างมันขึ้นมาในไฟล์และใช้ไฟล์นั้นทุกครั้งที่คุณเรียกใช้งาน ด้วยวิธีนี้ เราจะได้รับข้อมูลที่ "รบกวน" บางส่วน แต่เรายังคงสามารถแก้ไขข้อผิดพลาดได้
การทดสอบแบบกลุ่ม
เราสามารถรวบรวมการทดสอบหลายพันรายการที่ใช้เวลาหลายชั่วโมงในการดำเนินการได้อย่างง่ายดาย ไม่มีอะไรผิดปกติ แต่การจัดกลุ่มการทดสอบเหล่านี้ช่วยให้เราดำเนินการทดสอบชุดหนึ่งได้อย่างรวดเร็ว และตรวจสอบข้อกังวลหลักๆ จากนั้นจึงดำเนินการทดสอบทั้งชุดในเวลากลางคืน
การเขียน API ที่แข็งแกร่งและการทดสอบที่แข็งแกร่ง
การเขียน API และการทดสอบที่มีประสิทธิภาพเป็นสิ่งสำคัญ เพื่อไม่ให้เสียหายง่ายเมื่อมีการเพิ่มฟังก์ชันใหม่หรือฟังก์ชันการทำงานที่มีอยู่ได้รับการแก้ไข ไม่มีกระสุนวิเศษสากล แต่หลักทั่วไปคือการทดสอบว่า "การแกว่ง" (ล้มเหลวบางครั้ง สำเร็จบางครั้ง ซ้ำแล้วซ้ำเล่า) ควรละทิ้งอย่างรวดเร็ว
การทดสอบหน่วยมีความสำคัญอย่างยิ่งต่อ
วิศวกร เป็นรากฐานสำหรับกระบวนการพัฒนาแบบ Agile (ซึ่งเน้นหนักไปที่การเขียนโค้ด เนื่องจากเอกสารประกอบต้องมีหลักฐานบางอย่างที่แสดงว่าโค้ดทำงานตามข้อกำหนด) การทดสอบหน่วยให้หลักฐานนี้ กระบวนการเริ่มต้นด้วยการทดสอบหน่วย ซึ่งกำหนดฟังก์ชันการทำงานที่โค้ดควรใช้ แต่ในปัจจุบันไม่ได้ใช้งาน ดังนั้นการทดสอบทั้งหมดจะล้มเหลวในขั้นต้น จากนั้นเมื่อโค้ดใกล้จะเสร็จสมบูรณ์ การทดสอบก็ผ่านไป เมื่อการทดสอบทั้งหมดผ่าน โค้ดจะสมบูรณ์มาก
ฉันไม่เคยเขียนโค้ดขนาดใหญ่หรือแก้ไขบล็อกโค้ดขนาดใหญ่หรือซับซ้อนโดยไม่ใช้การทดสอบหน่วย ฉันมักจะเขียนการทดสอบหน่วยสำหรับโค้ดที่มีอยู่ก่อนที่จะแก้ไข เพียงเพื่อให้แน่ใจว่าฉันรู้ว่าฉันกำลังพัง (หรือไม่พัง) เมื่อฉันแก้ไขโค้ด สิ่งนี้ทำให้ฉันมั่นใจมากว่าโค้ดที่ฉันมอบให้กับลูกค้าของฉันทำงานอย่างถูกต้อง แม้ในเวลาตี 3 ก็ตาม