Lets Write Some Tests with Testify.php
Martin Angelov
jQuery Trickshots is our new epic jQuery tips and tricks book. Check it out!If you have only recently started out with web development, testing your code is probably not very high on your list. You write something that does what you need to, refresh your browser, and quickly find out if it works or not. Testing is more of a chore than anything else. The less you do of it the better. And who knows, someday, when you become more experienced you won’t even need to test – we all know rockstar programmers never make mistakes.
So very wrong!
But there is something right in the paragraph above. Testing does feel like a chore. A hopelessly boring and tedious one at that. There isn’t much you can do about it, unless you start seeing it in terms of game mechanics. Tests should be your scoreboard and each passed test should be an achievement you are proud of. This is where a small framework I wrote the other day comes into play.
Introducing Testify.php
Testify is a micro testing framework for PHP, released under the GPL license. It aims to be elegant and easy to use. Testify takes advantage of PHP 5.3′s anonymous function syntax to make defining tests almost JavaScript-like. Tests are logically grouped in test cases. The collection of test cases is considered a test suite. Now lets see how we can use this micro framework to make our development lives easier.
Shall we write some tests?
As a short example, we will be writing a PHP class for converting timestamps like the ones you get from a date/time database column (“15-10-2011 22:32″), into relative time strings (“1 month ago”). This is the sort of class that requires a lot of testing to get right, which would be a great use case for Testify.
The RelativeTime class
There are a lot of ways you can calculate relative time offsets. You could simply dump a pile of if/else statements and it still might work, but the code would be really hard to maintain and bug prone. However, we will go with something a bit more elegant. We will use the fact that each period we use to measure time with, is fixed in relation to a lesser one. In other words one day has exactly 24 hours, each hour has 60 minutes and each minute has 60 seconds.
Here is the skeleton of our class that uses this observation:
RelativeTime.class.php
class RelativeTime{
// The period names
private $names = array('second','minute','hour','day','week','month','year');
// How many times is the current period bigger than the previous one
private $divisions = array(1,60,60,24,7,4.34,12);
private $time = NULL;
public function __construct($timestr = NULL){
// You can pass a timestamp when constructing an object
}
public function getOffsetFrom($timestr = NULL){
// This method calculates the relative string.
// This is where the magic happens.
}
public function __toString(){
// This is one of the convenient PHP magic methods
}
private function timestampFromString($time){
// This private method will parse the time string
// into a valid time offset (in seconds)
}
}
Now lets write our tests.
Using Testify
There is a software development process called Test Driven Development (TDD), according to which you should write your tests first for best results. Lets explore how this would work with the framework.
The first step is to download Testify. After this, extract the testify folder and include it in a new PHP file, along with the RelativeTime class:
index.php
include 'RelativeTime.class.php'; include 'testify/testify.class.php';
Now the stage is set for a new test suite. The first step is to create an instance of the Testify class.
$tf = new Testify("Testing RelativeTime with Testify");
The string you pass here is used as the title of the suite (and displayed on top of the page). We can now move on to defining the test cases (logical collections of tests). The first test is more general and oriented to the functionality of the class as a whole.
index.php
$tf->test("General tests of the class", function($tf){
$relative = new RelativeTime();
// We haven't specified a timestamp yet.
$tf->assert($relative == "Timestamp not specified!");
try{
// Should throw an exception
$relative->getOffsetFrom();
// If we get to here, it means the test has failed:
$tf->fail();
}
catch (Exception $e){
$tf->assert($e->getMessage() == "Timestamp not specified!");
}
try{
// Should work
$relative->getOffsetFrom("22-10-2011");
$tf->pass();
}
catch (Exception $e){
$tf->fail();
}
});
The RelativeTime class throws an exception if we are accessing the relative time string but we haven’t yet passed a timestamp (either to the constructor or to the getOffsetFrom() method). But as the __toString magic method cannot throw exceptions, we will only return the exception text.
You can see some of the testing methods that testify supports – assert(), pass() and fail(). You can read more about them in the documentation.
We will now need to add a second test case, which tests the validity of the generated relative time strings.
index.php
$tf->test("Testing the relative time functionality", function($tf){
// Testing the class with a string timestamp
$relative = new RelativeTime(timestamp(time()-130));
// Testing the getOffsetFrom method
$tf->assert($relative->getOffsetFrom() == "2 minutes ago");
// Testing using the __toString conversion
$tf->assert($relative == "2 minutes ago");
// Quick and dirty tests
$tf->assert( new RelativeTime( time()) == "just now");
$tf->assert( new RelativeTime( time()-11) == "11 seconds ago");
$tf->assert( new RelativeTime( time()-59) == "59 seconds ago");
$tf->assert( new RelativeTime( time()-60) == "1 minute ago");
$tf->assert( new RelativeTime( time()-89) == "1 minute ago");
$tf->assert( new RelativeTime( time()-90) == "2 minutes ago");
$tf->assert( new RelativeTime( time()-30*60) == "30 minutes ago");
$tf->assert( new RelativeTime( time()-59*60) == "59 minutes ago");
$tf->assert( new RelativeTime( time()-60*60) == "1 hour ago");
$tf->assert( new RelativeTime( time()-90*60) == "2 hours ago");
$tf->assert( new RelativeTime( time()-86400) == "1 day ago");
$tf->assert( new RelativeTime( time()-3*86400) == "3 days ago");
$tf->assert( new RelativeTime( time()-9*86400) == "1 week ago");
$tf->assert( new RelativeTime( time()-29*86400) == "4 weeks ago");
$tf->assert( new RelativeTime( time()-31*86400) == "1 month ago");
$tf->assert( new RelativeTime( time()-100*86400) == "3 months ago");
$tf->assert( new RelativeTime( time()-350*86400) == "12 months ago");
$tf->assert( new RelativeTime( time()-365*86400) == "1 year ago");
$tf->assert( new RelativeTime( time()-20*365*86400) == "20 years ago");
});
// Helper function for constructing string timestamps
function timestamp($unixTime){
return date('r',$unixTime);
}
Great! What is left is to call the run() method and we get a pretty (albeit failing) test report.
$tf->run();
Now we can move on with writing the actual methods of the class. You can see the complete class implementation below:
class RelativeTime{
// The period names
private $names = array('second','minute','hour','day','week','month','year');
// How many of the previous period are contained in the next
private $divisions = array(1,60,60,24,7,4.34,12);
private $time = NULL;
public function __construct($timestr = NULL){
// You can pass a timestamp when constructing an object
$this->timestampFromString($timestr);
}
public function getOffsetFrom($timestr = NULL){
// This method calculates the relative string
$this->timestampFromString($timestr);
if(is_null($this->time)){
throw new Exception("Timestamp not specified!");
}
$time = $this->time;
$name = "";
if($time < 10){
return "just now";
}
for($i=0; $i<count($this->divisions); $i++){
if($time < $this->divisions[$i]) break;
$time = $time/$this->divisions[$i];
$name = $this->names[$i];
}
$time = round($time);
if($time != 1){
$name.= 's';
}
return "$time $name ago";
}
public function __toString(){
// __toString cannot throw exceptions
try{
return $this->getOffsetFrom();
}
catch(Exception $e){
return $e->getMessage();
}
}
private function timestampFromString($time){
if(is_numeric($time)){
// a unix timestamp (number of seconds since 1st Jan 1970)
$this->time = time() - $time;
}
else if(is_string($time)){
// a string timestamp
$this->time = time() - strtotime($time);
}
}
}
This gives us a satisfying all-green test page. Achievement unlocked!
Done!
Testing is necessary. It can give you peace of mind when changing existing code or help you in debugging new one. Hopefully this small framework will make testing more fun!
As always, be sure to share your thoughts and suggestions in the comment section.


9 Comments
What about PHPUnit ^^
I find its syntax to be too verbose. This was one of the frameworks that make testing a chore for me.
Yeah...could be. But until now the only testing framework for php (except Testify now :) And i like the interaction with maven for php. But that should also be working with Testify. Will try that tomorrow ^^
Any plans for Mocking? I would find it difficult to test non-trivial apps without mocks.
Mocking would be an excellent feature, I agree with Taylor that without it it would be hard to test larger apps.
Going to try it now for a discrete class I am writing.
Is not bad! When is possible is very important doing tests!
This is really nice, I will for sure be giving this a try.
PHPUnit turned me off for the same reason, its too complex for those just getting into unit testing. This looks like its pretty straight forward and easy to use. I'm actually excited about unit testing now.
can someone explain to me what's the point of 'testing' functions against a set of known and expected results? I mean, I'm new to TDD and unit testing in general, but reading this article has left me with more questions than answers... sorry if I sound rude, but I'd love it if someone would prove me wrong and convince me this actually makes sense!
@pierlo Im a n00b to TDD also, but the benefit is you try to make your tests fail but the end result is you want all of your tests to PASS.
So the hard part I think is coming up with good tests to try to make your class/object FAIL so you can find bugs.
Take a method/function getName() and setName(). You assume that when you run $this->setName('Brandon') that when you run echo $this->getName() you SHOULD get back Brandon, thats expected. If you DONT then the test fails because you have a bug.
Thats about the most complex example I know, Ive written like two test cases so just have a fuzzy general idea of how it all works heh.