Unit Testing in Kohana
- Posted by dlib on March 14th, 2008 filed in Code examples, ORM, Subversion, Tutorials
As announced earlier, Kohana has a Unit Testing library in subversion. I’ll try to guide you through some of its aspects. In a comment to another post I gave an example of the observer pattern working with ORM and I showed how it could be used to set modified and created fields automatically in ORM. So new records get a datetime in their modified and created fields, updated records just get a newly set modified field. I use datetime here because it’s easy to comprehend but it’s not that difficult to implement the same for mysql or php timestamps.
So, we’re building this method using unit tests. First install the unit test module, you can find a copy in the repository. Copy it into a modules directory and register the module in config/config.php. Now, edit one of your controllers to say
public function some_method() { $test=new Unit_Test(APPPATH.'tests'); echo $test; }
Open up the controller and you’ll see an Example_Test being run. The tests are found in MODPATH.’unit_test/tests’ and this is set in the config/unit_test.php file. You can delete the test if you want since you no longer need it.
We create a new test ORM_Test.php and put it in ‘application/tests’. We’ll start out with a simple test.
Notethe code examples require an ORM::exists() method to work.
ORM_Test.php
<?php defined('SYSPATH') or die('No direct script access.'); // test_path/Example_Test.php class ORM_Test extends Unit_Test_Case { public function setup() { $this->new_object=new Article_Model; $this->old_object=new Article_Model(1); //Set some properties to revert to later $this->old_dt= $this->old_object->modified; $this->old_title= $this->old_object->title; } public function new_object_not_exists_test() { $this->assert_false($this->new_object->exists()); } public function old_object_exists_test() { $this->assert_true($this->old_object->exists()); } public function teardown() { unset($this->new_object); unset($this->old_object); } }
These are some simple tests, they simple check if the ORM objects exists. The first one shouldn’t, the latter should, however if all is well Article_Model shouldn’t exist at all and an error is triggered when trying to instantiate it. Testing happens throughout development. You first write a test such as the above, you run it, see it fails and then you decide to write the Article_Model, it still fails, then you add record with id=1.
More tests
public function new_object_modified_created_set_test() { //something's gotta change before saving $this->new_object->title=uniqid(); $this->new_object->save(); //the field is datetime so needs conversion to php timestamp $created_ts=strtotime($this->new_object->created); $this->assert_integer($created_ts); //you could a custom is_timestamp method to the assertions //or use the regex assertion $modified_ts=strtotime($this->new_object->modified); $this->assert_integer($modified_ts); $this->assert_equal($modified_ts,$created_ts); //Revert back $this->new_object->delete(); } public function old_object_modified_changed_test() { $old_ts=strtotime($this->old_dt); $this->assert_integer($old_ts); //you need to change the object so saving makes sense $this->old_object->titel=uniqid('',true); $this->old_object->save(); $new_ts=strtotime($this->old_object->modified); $this->assert_integer($new_ts); //Another test could be that the new time is newer than the old time $this->assert_not_equal($old_ts,$new_ts); //Revert back to the original object $this->old_object->modified=$this->old_dt; $this->old_object->title=$this->old_title; $this->old_object->save(); }
Now, if you wrote the models correctly and added the record, the first two tests will pass but the latter two won’t. The tests themselves might need some work but I hope the idea is clear, some extra tests might be necessary but still. Let’s look at the failures and fix them. We adapt the save method in ORM a bit
MY_ORM.php
/** * Saves the current object. * * @return bool */ public function save() { // No data was changed if (empty($this->changed)) return TRUE; //I only add this line! Event::run(get_class($this).'.before_save'); $data = array(); foreach($this->changed as $key) { // Get changed data $data[$key] = $this->object->$key; } //the remainder of the save() method goes here and has not changed } /** * Simple exists method to see if model exists * * @return boolean */ public function exists() { return $this->object->id > 0; }
Now your Article_Model needs an adaptation:
models/article.php
class Article_Model extends ORM{ public function __construct($id=false) { parent::__construct($id); Event::add('Article_Model.before_save',array($this,'set_datetime')); } }
Now we added a method to be called just before saving the object, this method we still have to write though.
Again in models/article.php add the following method:
public function set_datetime() { $time=time(); if(!$this->exists()) { //if record doesn't exist it must be created with a time of creation $this->created=gmdate("Y-m-d H:i:s", $time); } //Always set a new modifed time $this->modified=gmdate("Y-m-d H:i:s", $time); }
Now if you run the tests, all tests should pass. You’ve set the requirements of your code, coded it and it has passed the requirements.
The workflow of unit testing might still not seem clear and it may seem like a lot of extra code. However, with big applications it will be rewarding. I’ll explain the art of testing a little more.
First issue is that with unit testing you can set the requirements of a class or a method beforehand. You assume a certain method or class exists and start using its syntax. The behaviour of a class is thus set before you write the class. You write some tests to determine what the methods output should be. Then you run the test case and you encounter a bunch of errors, only then you can start writing the code to solve the first error. This way you can run through all errors and in the end you end up with a perfectly working class or method. It’s a simple but effective way to set targets and check each of them as the tests pass. Let’s also not forget the reward of seeing the green screen when all tests pass
Need more information, one of the better explanations of unit testing I found at phpbuilder.
What else does this tutorial do? I’ve shown how you can implement the observer pattern in your orm models. I have now shown how you add a before_save hook, but what keeps you from doing an after_save, before_delete, before_update, after_insert etc. You can also set a property $invalidate in the ORM class so your hooks can invalidate the model to prevent it from saving, deleting, updating etc.
Another method to achieve the same behaviour is adding a before_save() method to your MY_ORM.php, you call it in the same place I called the observer. This method can do a similar thing as shown in this tutorial but isn’t as flexible. I think I’ll come back to this shortly with some other nifty features.
March 15th, 2008 at 4:04 pm
I can’t find your contact information so I just write it here:
In your article about jQuery & Forge validation you have a broken link to your jQuery library. Could you be so kind to put this library somewhere?
Btw, good blog! No much Kohana resources on the web..
March 15th, 2008 at 5:45 pm
The right link should be jQuery validation I no longer support this library since I wrote something similar to Forge, Formation which does the same thing and also needed jQuery validation.
March 15th, 2008 at 8:56 pm
Great introduction to the new Unit Test module.
Although, not related to this post, there seems to be a lot of questions about how to perform file uploading and resizing in Kohana. This would be great and very needed tutorial. I myself, would be especially interested an example of uploading images, resizing and creating a thumbnail version. In CI there was a very good Upload class that no longer exists in Kohana.
Thanks.
March 15th, 2008 at 11:31 pm
I’d use my Formation/Validation for that stuff. My validation lib should be able to handle the rules (allow, required etc) but I have not tested it. The Formation library I have tested and it did work. I’ve not yet used any uploading in real projects so I don’t know yet what problems in usage, syntax or anything else I might run into.
If I do see usage for the image library or file uploading I will cover it in a tutorial but I don’t expect I will use Kohana’s svn validation library for that but my own.
March 16th, 2008 at 12:18 am
Thx for reply. Are you going to release your new Formation/Validation library in some open licence? I would understand if not, I am just curious.
March 16th, 2008 at 9:31 am
It is released, I’ve not thought of licensing yet but consider it BSD. Anyway, Formation
March 16th, 2008 at 11:29 am
Thanks, but I am getting following error when creating basic input or textarea items (I use examles from docs):
htmlspecialchars() expects parameter 1 to be string, array given
Error occurred at line 26 of C:/wamp/www/cms/system/helpers/html.php
Latest html.php file from trac didn’t help.
March 16th, 2008 at 1:57 pm
I’ll look into it
March 17th, 2008 at 1:06 am
Could you post the code you’re using. I have not yet been able to replicate the problem.
March 19th, 2008 at 7:07 pm
Yes:
$form = new Formation;
$form->add_element(’input’,'name’);
that’s all. (Kohana version 2.1.1)
March 19th, 2008 at 7:08 pm
Or could you zip an archive with Kohana & Formation together where it works?
March 19th, 2008 at 7:48 pm
I plan to work some work on the library tonight and hope to release a new version as well.
I fixed the bug btw