Use unit tests to check the PHP code on each layer

Source: Internet
Author: User
Tags getmessage connect mysql new features object model pear php code strlen

Test-driven Development and unit testing are the latest ways to ensure that your code is still working as we expect after modifications and significant adjustments. In this article, you'll 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?

The WEB application is running 24x7, so my program is still running and the problem will haunt me at night. Unit tests have helped me build enough confidence in my code-so I can get a good night's sleep.

A unit test is a framework for writing test cases for your code and automatically running those tests. Test-driven development is a unit-testing approach, with the idea that you should first write the test program and verify that the tests can detect errors before you start writing code that requires these tests. When all the tests are passed, the features we develop are completed. The value of these unit tests is that we can run them at any time-before the code is checked in, after major modifications, or after you deploy to a running system.

PHP Unit Test

For PHP, the unit test framework is PHPUnit2. You can use the Pear command line as a pear module to install this system:% pear Install PHPUnit2.

After you install this framework, you can write unit tests by creating test classes that are derived from phpunit2_framework_testcase.

Module Unit Test

I find that the best place to start unit testing is in the business logic module of the application. I used a simple example: this is a function that sums up two numbers. To start the test, we first write the test cases, as shown below.

Listing 1. testadd.php

<?php
Require_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, all using the test prefix. Each method defines a test that can be as simple or complex as listing 1. In this case, we simply concluded in the first test that 1 plus 2 equals 3, and that in the second Test 1 plus 1 equals 2.

The PHPUNIT2 system defines the Asserttrue () method, which is used to test whether the conditional value contained in the parameter is true. We then wrote the add.php module, which initially caused it to produce the wrong result.


Listing 2. add.php

<?php
function add ($a, $b) {return 0;}
?>

Both tests fail when you run the unit test now.

Listing 3. Test failed

% PHPUnit testadd.php
PHPUnit 2.2.1 by Sebastian Bergmann.

Ff

time:0.0031270980834961
There were 2 failures:
1) test1 (Testadd)

2) test2 (Testadd)

Failures!!!
Tests Run:2, Failures:2, errors:0, incomplete tests:0.

Now I know both of these tests are working properly. Therefore, you can modify the Add () function to really do the actual thing.

<?php
function add ($a, $b) {return $a + $b;}
?>

Both of these tests are now available.

Listing 4. Test passed

% PHPUnit testadd.php
PHPUnit 2.2.1 by Sebastian Bergmann.

..

time:0.0023679733276367

OK (2 Tests)
%

Although the example of this test-driven development is very simple, we can appreciate its thinking. We first created the test case and had enough code to run the test, but the result was wrong. Then we verify that the test did fail, and then implement the actual code to make the test pass.

I've found that when I implement code I keep adding code until I have a complete test that overwrites all the code paths. At the end of this article, you'll see some suggestions for what tests to write and how to write them.

Database test

After the module test, the database access test can be done. Database access testing brings two interesting questions. First, we must restore the database to a known point before each test. Second, note that this recovery may cause damage to an existing database, so we must test the production database or, when writing test cases, be careful not to affect the contents of the existing database.

The unit test for a database starts with a database. To illustrate this issue, 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 a authors table with an associated ID for each record.

Next, you can write a test case.

Listing 6. testauthors.php

<?php
Require_once ' dblib.php ';
Require_once ' phpunit2/framework/testcase.php ';

Class Testauthors extends Phpunit2_framework_testcase
{
  function Test_delete_all () {
      $this->asserttrue (Authors::d elete_all ());
 }
  Function Test_insert () {
     $this->asserttrue (Authors::d elete_all ());
     $this->asserttrue (Authors::insert (' Jack '));
 }
  Function Test_insert_and_get () {
     $this->asserttrue (Authors::d elete_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 overrides features such as deleting an author from a table, inserting an author into a table, and inserting an author while verifying that the author exists. This is an additive test and I find it useful for finding bugs. By looking at which tests work and which tests are not working correctly, you can quickly find out what went wrong and then understand the difference.

The version of the dblib.php PHP database access code that initially generated the failure is shown below.

Listing 7. dblib.php

<?php
Require_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;
}
}
?>


Performing a unit test on the code in Listing 8 shows that all 3 tests failed:

Listing 8. dblib.php

% PHPUnit testauthors.php
PHPUnit 2.2.1 by Sebastian Bergmann.

FFF

time:0.007500171661377
There 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 code that correctly accesses the database--one way to add it--until all 3 tests are available. The final version of the dblib.php code is shown below.

Listing 9. The Complete dblib.php

<?php
Require_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;
}
}
?>

When you run a test on this code, all the tests run without problems, so that we can know that our code works correctly.

HTML Test

The next step in testing the entire PHP application is to test the Hypertext Markup Language (HTML) interface on the front end. To do this test, we need a Web page like the one shown below.


This page is summed up with two digits. To test this page, we start with the unit test code first.

Listing 10. testpage.php

<?php
Require_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 ('/}
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 ('/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 simpler than the built-in PHP Client URL Library (CURL), but you can use the latter.

There is a test that checks the returned page and determines whether the page contains HTML. The second Test asks for the calculation of 10 and 20 by placing the value in the requested URL, and then checks the results in the returned page.

The code for this page is shown below.


Listing 11. testpage.php

<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>

This page is fairly simple. Two input fields show the current value provided in the request. The result span shows the value of the two values. The <span> tag marks all the differences: it is invisible to the user, but is visible to the unit test. Therefore, unit testing does not require complex logic to find this value. Instead, it retrieves the value of a particular <span> tag. So when the interface changes, as long as span exists, the test can be passed.

As before, write the test case first, and then create a failed version of the page. We test the failure situation and then modify the contents of the page to make it work. The results are as follows:


Listing 12. Test failures, and then modify the page

% PHPUnit testpage.php
PHPUnit 2.2.1 by Sebastian Bergmann.

..

time:0.25711488723755

OK (2 Tests)
%

Both of these tests can be passed, which means that the test code can work correctly.

But there is a flaw in the test of the HTML front end: JavaScript. The Hypertext Transfer Protocol (HTTP) client code retrieves the page, but does not execute JavaScript. So if we have a lot of code in JavaScript, we have to create user agent-level unit tests. I've found that the best way to implement this functionality is to use the microsoft®internet explorer® embedded automation layer functionality. By using the Microsoft Windows® script written in PHP, you can use the Component Object Model (COM) interface to control Internet Explorer so that it navigates between pages. Then use the Document Object Model (DOM) method to find the elements in the page after performing a specific user action.

This is the only one by one ways I've learned to unit test the front-end JavaScript code. I admit that it's not easy to write and maintain, and that these tests can be easily compromised even when the page changes slightly.

What tests to write and how to write those tests

When writing tests, I like to cover the following situations:

All positive tests
This set of tests ensures that everything works as we expect it to.
All negative tests
Use these tests one by one to ensure that each failure or exception condition is tested.
Positive sequence test
This set of tests ensures that calls in the correct order can work as we expect.
Negative Sequence Test
This set of tests ensures that when calls are not in the correct order, they fail.
Load test
Where appropriate, a small set of tests can be performed to determine the performance of these tests within our desired range. For example, 2,000 calls should be completed within 2 seconds.
Resource Testing
These tests ensure that the application programming interface (API) can properly allocate and release resources-for example, several successive calls to open, write, and close file-based APIs to ensure that no files are still open.
Callback Test
For APIs with callback methods, these tests ensure that if no callback function is defined, the code can run correctly. In addition, these tests ensure that the code still works correctly when the callback function is defined but the callback functions are incorrect or an exception is generated.
This is a few ideas about unit testing. I also have a few suggestions on how to write unit tests:

Do not use random data
Although it may seem like a good idea to produce random data in an interface, we should avoid doing so because the data becomes very difficult to debug. If the data is randomly generated on each call, it is possible to produce a test with an error and another test without error. If the test requires random data, you can generate the data in a single file, and then use the file each time you run it. With this approach, we get some "noise" data, but we can still debug errors.
Group Test
It's easy to accumulate thousands of tests and it will take several hours to finish. That's fine, but grouping these tests allows us to run a group of tests quickly and examine the main concerns, and then run the full test at night.
Write robust APIs and robust testing
It is important to note that when writing APIs and tests, they cannot easily crash when adding new features or modifying existing functionality. There is no universal trick, but one rule is that "oscillating" tests (a failure, a moment of success, repetitive tests) should be discarded quickly.

Conclusion

Unit tests are significant for engineers. They are the agile development process, which emphasizes the role of coding because the document requires some evidence that the code is working according to the specification. This evidence is provided by unit tests. This process starts with unit testing, which defines the functionality that the code should implement but is not yet implemented. As a result, all tests will initially fail. Then when the code is nearing completion, the test passes. When all the tests pass, the code becomes perfect.

I have never written large code or modified large or complex blocks of code without using unit tests. I usually write unit tests for existing code before I modify the code to make sure that I know what I'm breaking (or not destroying) when I modify the code. This gives me a lot of confidence in the code I provide to my customers, believing that they are working correctly-even at 3 o'clock in the morning.



Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.