TDD (Test Driven Development) concept is common practice of
most of the companies which involve with the long time projects. It
helps to keep the static behavior of functions over the development
period. The functions are implemented after implementing test methods
(unit tests) for that function. After implementing the function, run the tests and if any test is failing, again modify the function. This process repeats until the all tests are passing.
Test method is a function which check whether the specific behavior of a
function is correct. Any function can have at least two test methods. one
positive test and one negative test. Most probably a function may have
many test methods. The class in which all the test methods are placed is
called Test Case. By setting up a test suite for this class, the all
tests methods can be run at once.
In long time
projects like open source projects, the functions are frequently
modified by the developers during a long time. If the modified function
is not given the previous behavior of that function, the output for other places will be going wrong. It can be cause to failures. By
writing tests this problem can be reduced. Before and after modifying
the test cases, by running the tests, developer can be verify the
function behave correctly. As well by going through the test cases helps
even understand what actually function does.
To follow this TDD concept with Java, JUnit framework can be used. As
well with PHP, PHPUnit framework can be used. Let's do some experiments
with PHPUnit with Ubuntu OS.
Install PHPUnit framework
$sudo apt-get install phpunit
Now create your test case as a sub class of 'PHPUnit_Framework_TestCase' having just one dummy method to verify we can forward without issue.
<?php require_once 'PHPUnit/Framework.php'; // installed file with phpunit framework which is located at : /usr/share/php/PHPUnit/ class MyClassTest extends PHPUnit_Framework_TestCase { // This function just return true function testCheckAllWorksFine() { $this->assertTrue(true); } } ?>
Run this MyClassTest.php file as :
$phpunit MyClassTest.php
If the result is as following (Fig. 1), you can proceed the work with phpunit.
Fig. 1 |
Characters it returns :
. : pass
F : fail
E : error
The functions starting from 'assert' is implemented in the framework. As well there are more useful functions we can use with this framework. You can find more assert functions by visiting http://www.phpunit.de/manual/3.6/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.assertions.
I have used following useful assert functions for this article :
assertTrue() : Check whether the parameter is True
assertFalse() : Check whether the parameter is False
assertEqual() : Match two results
Start work
1. Requirement : need a function which accept two strings and return the length of the most lengthy string, if both arguments are not strings return false
* First just declare your function properly with arguments
<?php class MyClass { function getMaxLength ($str1, $str2) { } } ?>
* Now start to write test methods,
a). Check the arguments
<?php require_once 'MyClass.php'; require_once 'PHPUnit/Framework.php'; class MyClassTest extends PHPUnit_Framework_TestCase { var $obj; // This function calls before any test merhod run function setUp() { $this->obj = new MyClass(); } // Positive : Test getMaxLength for correct arguments function testGetMaxLengthForAtLeastOneIsString() { $paramArray = array("str1", 2); $result = $this->obj->getMaxLength($paramArray[0], $paramArray[1]); $this->assertEquals(4, $result); } // Negative : Test getMaxLength for non of arguments are string function testGetMaxLengthForNoStrings() { $paramArray = array(array(1,2), 12.3); $result = $this->obj->getMaxLength($paramArray[0], $paramArray[1]); $this->assertFalse($result); } } ?>
The setUp() will call automatically
before running any test method. It can be used for create a fresh
objects, load data for databases if using etc. All test methods must be
start with 'test'. Using the function name which is going to use by
specific test method regardless of the length of the test method name,
will be increase the readability and understandability while reviewing
later.
b). Check whether the function returns an integer
// Test getMaxLength for output integer for correct arguments function testGetMaxLengthForCorrectReturnType() { $paramArray = array("str1", 2); $result = $this->obj->getMaxLength($paramArray[0], $paramArray[1]); $this->assertTrue(is_int($result)); }
c). Check whether is returns the length of the most lengthy string
// Test getMaxLength for Length of the lengthy string function testGetMaxLengthForCorrectLength() { $paramArray = array("short_string", "lenghty_string"); $result = $this->obj->getMaxLength($paramArray[0], $paramArray[1]); $this->assertEquals(14, $result); }
Now
the most needed test cases are implemented. From the above test
methods, some scenarios like check the function
return length of the string for arguments having only one string, is
already covered. Now just run the test and
see most of them are failing. Its time to start implement the actual
function in a
way that all te test cases are passing.
Function can be written as :
function getMaxLength($str1, $str2) { if(is_string($str1) || is_string($str2)) { if(is_string($str1) && is_string($str2)) { $max = (strlen($str1) >= strlen($str2)) ? strlen($str1) : strlen($str2); } elseif (is_string($str1)) { $max = strlen($str1); } else { $max = strlen($str2); } return $max; } return false; }
Now
again run the test. See whether all the test are passing or not. If not
your function may have some issues. Improve the function and again run
the test. Do this process until all the test are passing.
The
example we discussed was very easy one. But in real applications we
have use more complex algorithms and tactics dealing with databases. To
check the functionality of the data transaction functions, we can use a
test database with some preloaded data. Inside the setUp() function we
can create tables and load the data to the
test database. As well using the function tearDown() which automatically call after execution of any test method, we can clean the data from the database.
Sometimes
applications with MVC architecture have two or more separate layers in
Data Layer. In such kind of situation, functions need to test
independent from their layers. Lets assume in our application there are
two layers in Data Layer. One is data transaction layer which directly
deal with the database and other one is data processing layer which
process data before or after deal with database through data transaction
layer. So, while writing test for a function in data process layer
which get some data by calling a function in data transaction layer, we
should verify that the correct data set is returned from the function in
data transaction layer. To do this we can use Moc Objects.
Lets go through one example.
* getStrings() method in DataTransaction class return an array with two strings (No need of even declare this class)
* getProcessedResult() method in DataProcess
class, get the array of two strings by calling the above mentioned
function (getStrings()), and return the length of the most lengthy
string out of them. This class contain getter and setter methods for use
DataTransaction class.
(Declare the getProcessedResult() function
and implement getter and setter methods)
<?php class DataProcess { private $dataTransaction; function getDataTransaction() { if(is_null($this->dataTransaction)) { return new DataTransaction(); } return $this->dataTransaction; } function setDataTransaction($dataTransaction) { $this->dataTransaction = $dataTransaction; } function getProcessedResult() { } } ?>
* Now write the test class using the Moc Object of DataTransaction class
<?php require_once 'DataProcess.php'; require_once 'PHPUnit/Framework.php'; class TestMoc extends PHPUnit_Framework_TestCase { var $dataProcess; // This function calls before any test merhod run function setUp() { $this->dataProcess = new DataProcess(); } public function testGetProcessedResult() { // Create a Mock Object for the DataTransaction class // mocking only the getStrings() method. $dataTransaction = $this->getMock('DataTransaction', array('getStrings')); // Set up the expectation for the getStrings() method // to be called only once and with no arguments // and return value is array("abc", "abcde") $dataTransaction->expects($this->once()) ->method('getStrings') ->with() ->will($this->returnValue(array("abc", "abcde"))); // Set the mocked DataTransaction object to // DataProcess object using setter $this->dataProcess->setDataTransaction($dataTransaction); // Call the getProcessedResult() method on the $dataProcess object // which we expect to call the mocked DataTransaction object's // getStrings() method with no arguments. $this->assertEquals(5, $this->dataProcess->getProcessedResult()); } } ?>
Here, even we didn't implement (even declare) the DataTransaction class, we could run the test using Moc Object. So that we can test functions independent from the layer it contains and other needed functions.
Setting up test suite
Test suit can be used to mange which test cases should run. We can run even all the test methods in several test cases at once. Test suite can be create as following example code snippet.
<?php require_once 'PHPUnit/Framework.php'; class AllTests { public static function suite() { $suite = new PHPUnit_Framework_TestSuite("PHPUnit"); // Add needed test cases $suite->addTestFile("MyTestClass1.php"); $suite->addTestFile("MyTestClass2.php"); return $suite; } } ?>
With
PHPUnit framework the percentage of the functions which covers from the unit
tests can be obtained as a coverage report. Following command can be used to obtain a coverage report. (probably you need to install xDebug : http://xdebug.org/docs/install)
$phpunit --coverage-html ./report AllTests.php
From
this report we can find out the uncovered code segments. Some of code segments in MyClass.php has not covered from the unit tests. To cover
them again test can be implemented.
So, this is a
basic idea of how to follow test driven development with PHPUnit framework. Hope this helpful to you.
Nice article dude, keep it continue...
ReplyDelete