Test-driven development and unit testing are the latest ways to ensure that code continues to work as expected despite modifications and major tweaks. In this article, you will learn how to unit test your PHP code at the module, database, and user interface (UI) layers.
It's 3 o'clock in the morning. How do we know that our code is still working?
Web applications run 24x7, so the question of whether my program is still running will bother me at night. Unit testing has helped me build enough confidence in my code that I can sleep well.
Unit testing is a framework for writing test cases for your code and running these tests automatically. Test-driven development is a unit testing approach based on the idea that you should first write tests and verify that these tests can find errors, and only then start writing the code that needs to pass these tests. When all tests pass, the feature we developed is complete. The value of these unit tests is that we can run them at any time—before checking in the code, after a major change, or after deployment to a running system.
PHP Unit Testing
For PHP, the unit testing framework is PHPUnit2. This system can be installed as a PEAR module using the PEAR command line: % pear install PHPUnit2.
After installing the framework, you can write unit tests by creating test classes derived from PHPUnit2_Framework_TestCase.
Module Unit Testing
I have found that the best place to start unit testing is in the business logic module of the application. I'm using a simple example: this is a function that sums two numbers. To start testing, we first write the test case as shown below.
Listing 1. TestAdd.php
<?phprequire_once 'Add.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAdd extends PHPUnit2_Framework_TestCase{ function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); } function test2() { $this->assertTrue( add( 1, 1 ) == 2 ); }}?>
This TestAdd class has two methods, both of which use the test prefix. Each method defines a test, which can be as simple as Listing 1 or very complex. In this case, we simply assert that 1 plus 2 equals 3 in the first test, and 1 plus 1 equals 2 in the second test.
The PHPUnit2 system defines the assertTrue() method, which is used to test whether the condition value contained in the parameter is true. We then wrote the Add.php module, which initially produced incorrect results.
Listing 2. Add.php
<?phpfunction add( $a, $b ) { return 0; }?>
Now when running the unit tests, both tests fail.
Listing 3. Test failures
% phpunit TestAdd.phpPHPUnit 2.2.1 by Sebastian Bergmann.FFTime: 0.0031270980834961There were 2 failures:1) test1(TestAdd)2) test2(TestAdd)FAILURES!!!Tests run: 2, Failures: 2, Errors: 0, Incomplete Tests: 0.
Now I know both tests work fine. Therefore, the add() function can be modified to actually do the actual thing.
Both tests now pass.
<?phpfunction add( $a, $b ) { return $a+$b; }?>
Listing 4. Test passed
% phpunit TestAdd.phpPHPUnit 2.2.1 by Sebastian Bergmann...Time: 0.0023679733276367OK (2 tests)%
although This example of test-driven development is very simple, but we can understand its ideas. We first created the test case and had enough code to make the test run, but the result was wrong. Then we verify that the test indeed fails, and then implement the actual code to make the test pass.
I find that as I implement code I keep adding code until I have a complete test that covers all code paths. At the end of this article, you'll find some advice on what tests to write and how to write them.
Database testing
After module testing, database access testing can be performed. Database access testing brings up two interesting issues. First, we must restore the database to some known point before each test. Secondly, be aware that this recovery may cause damage to the existing database, so we must test on a non-production database, or be careful not to affect the content of the existing database when writing test cases.
Database unit testing starts from the database. To illustrate this problem, we need to use the following simple pattern.
Listing 5. Schema.sql
DROP TABLE IF EXISTS authors;CREATE TABLE authors (id MEDIUMINT NOT NULL AUTO_INCREMENT, name TEXT NOT NULL, PRIMARY KEY (id));
Listing 5 is an authors table, and each record has an associated ID .
Next, you can write test cases.
Listing 6. TestAuthors.php
<?phprequire_once 'dblib.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAuthors extends 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' ) ); } function test_insert_and_get() { $this->assertTrue( Authors ::delete_all() ); $this->assertTrue( Authors::insert( 'Jack' ) ); $this->assertTrue( Authors::insert( 'Joe' ) ); $found = Authors::get_all() ; $this->assertTrue( $found != null ); $this->assertTrue( count( $found ) == 2 ); }}?>
This set of tests covers deleting authors from the table and inserting authors into the table As well as functions such as inserting the author while verifying that the author exists. This is a cumulative test, which I find very useful for finding bugs. By observing which tests work and which don't, you can quickly figure out what's going wrong and then further understand the differences.
The version of the dblib.php PHP database access code that originally produced the failure is shown below.
Listing 7. 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()); } return $db; } public static function delete_all() { return false; } public static function insert( $name ) { return false; } public static function get_all() { return null; }}?>
Executing unit tests on the code in Listing 8 will show that all three tests fail:
Listing 8. dblib.php
% phpunit TestAuthors.phpPHPUnit 2.2.1 by Sebastian Bergmann.FFFTime: 0.007500171661377There were 3 failures:1) test_delete_all(TestAuthors)2) test_insert(TestAuthors)3) test_insert_and_get(TestAuthors)FAILURES!!!Tests run: 3, Failures: 3, Errors: 0, Incomplete Tests: 0.%
Now we can start adding the code to correctly access the database - method by method - until all 3 tests pass. The final version of dblib.php code is shown below.
Listing 9. Complete 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()); } return $db; } public static function delete_all() { $ db = Authors::get_db(); $sth = $db->prepare( 'DELETE FROM authors' ); $db->execute( $sth ); return true; } public static function insert( $name ) { $db = Authors::get_db(); $sth = $db->prepare( 'INSERT INTO authors VALUES (null,?)' ); $db->execute( $sth, array( $name ) ); return true; } public static function get_all() { $db = Authors::get_db(); $res = $db->query( "SELECT * FROM authors" ); $rows = array(); while( $res->fetchInto( $ row ) ) { $rows []= $row; } return $rows; }}?>
HTML Testing
The next step in testing the entire PHP application is to test the front-end Hypertext Markup Language (HTML) interface. To perform this test, we need a Web page like the one below.
Listing 10. TestPage.php
<?phprequire_once 'HTTP/Client.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestPage extends PHPUnit2_Framework_TestCase{ function get_page( $url ) { $client = new HTTP_Client(); $client ->get( $url ); $resp = $client->currentResponse(); return $resp['body']; } function test_get() { $page = TestPage::get_page( 'http://localhost/unit /add.php' ); $this->assertTrue( strlen( $page ) > 0 ); $this->assertTrue( preg_match( '/<html>/', $page ) == 1 ); } function test_add( ) { $page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' ); $this->assertTrue( strlen( $page ) > 0 ); $this-> assertTrue( preg_match( '/<html>/', $page ) == 1 ); preg_match( '/<span id="result">(.*?)</span>/', $page, $out ); $this->assertTrue( $out[1]=='30' ); }}?>
This test uses the HTTP Client module provided by PEAR. I find it a bit simpler than the built-in PHP Client URL Library (CURL), but the latter can also be used.
There is a test that checks the returned page and determines whether it contains HTML. The second test requests the sum of 10 and 20 by placing the value in the requested URL, and then checks the result in the returned page.
The code for this page is shown below.
Listing 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 >
This page is fairly simple. The two input fields display the current values provided in the request. The result span shows the sum of these two values. The markup marks all the differences: it is invisible to the user, but visible to the unit tests. So unit tests don't need complex logic to find this value. Instead, it retrieves the value of a specific tag. In this way, when the interface changes, as long as the span exists, the test will pass.
As before, write the test case first and then create a failing version of the page. We test for failure and then modify the content of the page to make it work. The results are as follows:
Listing 12. Test failure, and then modify the page
% phpunit TestPage.phpPHPUnit 2.2.1 by Sebastian Bergmann...Time: 0.25711488723755OK (2 tests)%
Both tests can pass, which means that the test code It works fine.
When running tests on this code, all tests run without issue, so we know that our code works correctly.
But testing the HTML front-end has a flaw: JavaScript. Hypertext Transfer Protocol (HTTP) client code retrieves the page, but does not execute the JavaScript. So if we have a lot of code in JavaScript, we have to create user agent level unit tests. The best way I've found to achieve this functionality is to use the automation layer functionality built into Microsoft® Internet Explorer®. With Microsoft Windows® scripts written in PHP, you can use the Component Object Model (COM) interface to control Internet Explorer to navigate between pages, and then use Document Object Model (DOM) methods to find pages after performing specific user actions. elements in .
This is the only way I know of to unit test front-end JavaScript code. I admit it's not easy to write and maintain, and these tests are easily broken when even slight changes are made to the page.
Which tests to write and how to write them
When writing tests, I like to cover the following scenarios:
All positive tests
This set of tests ensures that everything is working as we expect.
All negative tests
use these tests one by one to ensure that every failure or anomaly is tested.
Positive Sequence Testing
This set of tests ensures that calls in the correct sequence work as we expect.
Negative Sequence Testing
This set of tests ensures that calls fail when they are not made in the correct order.
Load Testing
Where appropriate, a small set of tests can be performed to determine that the performance of these tests is within our expectations. For example, 2,000 calls should complete within 2 seconds.
Resource Tests
These tests ensure that the application programming interface (API) is allocating and releasing resources correctly - for example, calling the open, write, and close file-based API several times in a row to ensure that no files are still open.
Callback tests
For APIs that have callback methods, these tests ensure that the code runs normally if no callback function is defined. In addition, these tests can also ensure that the code can still run normally when callback functions are defined but these callback functions operate incorrectly or generate exceptions.
Here are some thoughts on unit testing. I have a few suggestions on how to write unit tests:
Don’t use random data
.Although generating random data in an interface may seem like a good idea, we want to avoid doing it because this data can become very difficult to debug. If the data is randomly generated on each call, it may happen that an error occurs in one test but not in another test. If your test requires random data, you can generate it in a file and use that file every time you run it. With this approach, we get some "noisy" data, but we can still debug errors.
Group testing
We can easily accumulate thousands of tests that take several hours to execute. There's nothing wrong with that, but grouping these tests allows us to quickly run a set of tests and check for major concerns, and then run the full set of tests at night.
Writing Robust APIs and Robust Tests
It's important to write APIs and tests so that they don't break easily when new functionality is added or existing functionality is modified. There is no universal magic bullet, but a rule of thumb is that tests that "oscillate" (fail sometimes, succeed sometimes, over and over again) should be discarded quickly.
Conclusion
Unit testing is of great significance to engineers. They are a foundation for the agile development process (which places a heavy emphasis on coding because documentation requires some evidence that the code is working according to the specifications). Unit tests provide this evidence. The process starts with unit tests, which define the functionality that the code should implement but currently does not. Therefore, all tests will initially fail. Then when the code is nearly complete, the tests pass. When all tests pass, the code becomes very complete.
I've never written large code or modified a large or complex block of code without using unit tests. I usually write unit tests for existing code before I modify it, just to make sure I know what I'm breaking (or not breaking) when I modify the code. This gives me a lot of confidence that the code I provide to my clients is running correctly - even at 3am.