Testing Models in CakePHP - Now let's get rid of the unnecessary ModelTest classes !
Posted on 30/7/08 by Tim Koschützki
Hey folks,
today I committed some stuff that will make a bigger impact on your Unit Testing development in CakePHP. It's a much cleaner way of how you are able to test models. Up until now there was always a need to create a so-called test model that extends your model-under-test in order to overwrite its $useDbConfig setting to be 'test_suite'. By that you ensured that your models run with the test_suite datasource when the tests are run.
Nate proposed ClassRegistry::config(), which allows you to tell the ClassRegistry class which datasource it shall use when
ClassRegistry::init() is used the next time (and thereby a model is instantiated). So what we are doing now in the testsuite is telling the ClassRegistry to use the test_suite db config whenever a new model is instantiated via ClassRegistry::init(). Doesn't make sense to you? Well, let's look at an example.
The following code is directly taken from the CookBook and shows the old way of doing things:
class ArticleTest extends Article {
var $name = 'ArticleTest';
var $useDbConfig = 'test_suite';
}
class ArticleTestCase extends CakeTestCase {
var $fixtures = array('app.article_test');
function testPublished() {
$this->ArticleTest =& new ArticleTest();
$result = $this->ArticleTest->published(array('id', 'title'));
$expected = array(
array('ArticleTest' => array( 'id' => 1, 'title' => 'First Article' )),
array('ArticleTest' => array( 'id' => 2, 'title' => 'Second Article' )),
array('ArticleTest' => array( 'id' => 3, 'title' => 'Third Article' ))
);
$this->assertEqual($result, $expected);
}
}
So far so good. The ArticleTest model extends the imported Article model to overwrite the datasource setting. Then it loads the ArticleTest fixture to use its data throughout the test. The drawback: A whole class that does practically nothing. In a bigger system with plenty of models and plenty of tests this can contribute to your hair loss - especially when you forget to name your fixture properly or provide the $name property in the Test Model.
With the new code in place, this will just work as well:
class ArticleTestCase extends CakeTestCase {
var $fixtures = array('app.article');
function testPublished() {
$this->Article =& ClassRegistry::init('Article');
$result = $this->Article->published(array('id', 'title'));
$expected = array(
array('Article' => array( 'id' => 1, 'title' => 'First Article' )),
array('Article' => array( 'id' => 2, 'title' => 'Second Article' )),
array('Article' => array( 'id' => 3, 'title' => 'Third Article' ))
);
$this->assertEqual($result, $expected);
}
}
So we got rid of the ArticleTest class and can now use the normal ArticleFixture that we probably use throughout the system anyway to provide some standard data for the application. Via the $test_suite setting in your database.php file you can control where and how the test data is imported.
The only change to your code: Instantiate the subject under test via ClassRegistry::init() instead of instantiating directly via the new operator.
To me this seems like the proper way to do it. No need for further unnecessary classes. Everything clean and controllable. Do you agree?
-- Tim Koschuetzki aka DarkAngelBGE
PS: Kudos again to Nate for ClassRegistry::config() !
You can skip to the end and add a comment.
Great improvement!
What about associated models trough hasMany, belongsTo, ...? Right now I need to do something like this in the ArticleTestCase (let's assume Article hasMany Tags):
$this->ArticleTest->Tag = new TagTest();
So now it should be just $this->Article->Tag =& ClassRegistry::init('Tag') ?
Jose Lorenzo: :]
klevo: No even better. Now that the ClassRegistry is configured in the testsuite to pass the test_suite datasource to any ClassResgitry::init() call, when your subject-under-test is loaded and creates its linkedModel, they will also be instantiated with the test_suite datasource.
So you would really only use your models like you'd normally do... just that they run via a different datasource. :)
I use a trick to achieve this. I put in app_model.php the following code:
function __construct(){
if (isset($_GET['app'])){
$this->useDbConfig = 'test';
$this->cacheSources = false;
}
parent::__construct();
}
The isset($_GET['app']) is to see if some test runs
JoaoJose: Cool. :] Not needed anymore now though. :]
This is great news. I had some problems with testing Models and associated Models because of this "ModelTest"-extra-class-thing.
So I'm looking forward to test this :)
One question: Why are you using "ArticleTest" as array key instead of "Article"? Does ClassRegistry::init() automatically set the $name-property for the model under test to "*Test"?
Andreas: Nah, it's a typo. I corrected it. Thank you. :]
Nate and Tim should be given a medal. Hip hip hooray!
(BTW, I'm running CakePHP off SVN and the new registry works like a charm).
This is awesome. Thanks to Felix and Nate!
crux: I had nothing to do with this, thank Tim ; ).
crux: :(
; ]
Tim/Nate,
Thanks for addressing this issue, it helped out a lot. Tim, thanks for allowing me to bug you on IM with the issue until it was resolved. Keep up all the great work.
~rpeterson
You are most welcome Ryan. :) Thanks for bringing it up!
I've been fighting this in the background on and off for weeks trying to make my (previously working) test cases run again with my SVN head cake.
I followed the instructions to the letter but just kept getting DB problems. Typically you get a message like "Error: Missing database table 'flags' for model 'Flag'"
I was messing with it again this afternoon, and I don't know why it makes a difference, but I found I had to explicitly state the connection in the fixtures now, so something like below. I had caching off and all other tweaks you might expect. It was the difference between it working and not working.
class FlagTestFixture extends CakeTestFixture {
var $name = 'FlagTest';
var $import = array('model' => 'Flag', 'records' => true, 'connection' => 'default');
}
Without this it had previously worked, so perhaps something in the core changed ( i didn't have time to look unfortunately ) It doesn't really make sense why this behaviour might have changed
I don't think the code in the original blog article is anything to do with this but I thought I would mention it in case others see the same problem, getting the fixtures right is clearly a major part of getting the test cases to work. When it does, happy days all round.
PS the original article was a useful guide too - thanks
@Aitch: there could be a problem with your database or test configuration. The correct database connection for running tests is 'test_suite'. Perhaps your fixtures aren't getting initialized correctly?
I always thought that whole approach to testing in CakePHP was against the mantra "convention over configuration" - there was just too much to do before even writing first assert().
What is presented here really helps! Thanks guys.
With RC3 I get errors like this:
Unexpected PHP error [Trying to get property of non-object] severity [E_NOTICE] in [../cake/libs/class_registry.php line 295]
This is triggered by one of these lines:
- $this->ArticleTest =& new ArticleTest();
- $this->Article =& ClassRegistry::init('Article');
Is this maybe related ClassRegistry changes?
@Christof - I see the same error on my setup too, if you find a solution, do post. The test cases all seem to be working fine for me now. I can't quite nail it down why this exception occurs.
I will say that in my experience the test suite does work well, but is quite sensitive to naming of files and how you address those files, especially the $fixtures var.
I have to use something like var $fixtures = array('app.flag_test'); (note the _test suffix) and that seems to work great. The various blog entries / cakebook out there are a little inconsistent and vary slightly in how they address it.
There is a recent thread on the cake list ("more problems with RC3 and tests") that has some good pointers
As a result I upgraded to SVN head (7740) just now and the problem seems to have resolved itself - no more exception - so very happy again, everything is forgiven!!
Christof Damian: Why do you call $this->ArticleTest =& new ArticleTest();?
@Aitch: What are the fixture names that you use?
Glad it is fixed now for oyu guys, though. :)
@Tim: interesting you ask about fixture names. Depending on where you read, you see a variety of approaches. If the model name is called "Flag",,,
$fixtures = array('app.flag_test'); // Cookbook way
$fixtures = array('app.flag'); // Your way, as documented in article
$fixtures = array('flag');
I presume if is dependent on the original filename. Cookbook way works for me, but your documented way does not.
My persistent problem now is that with 7740 it definitely creates the tables, then definitely populates the (non-imported) fixture data into that table, but the test case class code methods never actually refer to it, choosing to stick with the production tables when doing queries. Presume I have to force useDBConfig or similar but this small detail is eluding me this morning.
I have a structure and nomenclature something like this. Am I missing something stupid?
app/tests/fixtures/cutoff_test_fixture.php
class CutoffTestFixture extends CakeTestFixture {
var $name = 'CutoffTest';
var $import = 'Cutoff';
var $records = array( .. handful of hand written records .. );
}
app/tests/cases/models/cutoff.test.php
App::Import('Model', 'Cutoff');
class CutoffTestCase extends CakeTestCase {
var $fixtures = array('app.cutoff_test');
function setUp() {
$this->CutoffTest = & ClassRegistry::init('Cutoff');
}
function testXYZ() {
// This find returns data from production tables, not the test fixture table
pr($this->CutoffTest->find('all'));
}
}
Aitch:
Well, you aren't supposed to put a 'Test' into the fixture names. :)
Have a look at some of the core fixtures, for example:
FlagTreeFixture
Then you load it via app.flag_tree.
Secondly, in your test case you have to load the data via $this->loadFixtures('FlagTree');
Also according to your naming $this->CutoffTest might refer to the fixture name and not the model name. That's why I prefer to store the subject under test (the model we want to test) in $this->Sut, which is also shorter.
Please tell me if this helped any. :)
The testing behaviour clearly continues to evolve and I am glad it does but it can be a bit frustrating and the various blogs etc contradict each other regularly. However the fact I keep posting here should prove whom I believe most!
Your pointers were helpful and allowed me to try some other ideas Tim, and I debugged it sufficiently to make it work - thank you, i appreciate your time.
The 'book' does suggest the use of "Test" in the fixture name which is why I tried to adopt that style but obviously it is not strictly necessary if you change everything else.
I think your point about possible confusion of object names / CutoffTest may also be relevant, but I did not actually go and prove that.
For the record, here is what I have done on SVN head 7740. I'm using HEAD as this fixes an testing exception bug discussed on the cake group list
I was able to demonstrate to myself that my simple fixture was definitely populating the test DB with the hand coded data by overriding CakeTestFixture::insert, deliberately die'ing after a call to the parent method then reviewing the DB. So a simple fixture, app/tests/fixtures/cutoff_fixture.php looks something like:
class CutoffFixture extends CakeTestFixture {
var $name = 'Cutoff';
var $import = 'Cutoff';
var $records = array( .... hand written selection of records .... )
}
For the Test Case, app/tests/cases/models/cutoff.test.php for some reason I had to force the use of test_suite again. Without it, the find results are pulled from the live (default) DB. This may be a bug, but I can't quite establish why right now.
App::Import('Model', 'Cutoff');
class CutoffTestCase extends CakeTestCase {
var $fixtures = array('app.cutoff');
function start() {
parent::start();
$this->Sut = & ClassRegistry::init('Cutoff');
$this->Sut->useDbConfig = 'test_suite';
}
function testXYZ() {
// Findings will vary depending on whether useDbConfig is set above.
// If useDbConfig is omitted - get results from default (live) DB
// If useDbConfig is set to test_suite, get results from $test DB config
pr($this->Sut->find('all'));
// assertSomething down here
}
}
Anyway I hope this will be my last post today now I have this mostly working!!!!!! :-)
Aitch: The Cookbook has not been updated to reflect the new functionality. Please help everyone out and submit an update to it. Thanks!
.... is it possible that NOONE really knows what's going on in your test fuck????
i'm now really fed up with your shit!!!
does anyone at all have a glue where the testsuite shall evolve to? What the big picture is? Or at least what the CURRENT standard is?
How is it possible that you release a RC3 and crack up more than it was before??? what do you expect me? to only rely on latest svn co's and having to fuck around on my own with anything buggy that's been commited next???
HELL!!! isn't it possible to release a single document that fully describes a test setup including a test class, fixture, model, controller, view?
I don't have the time debugging the cake core, ... it's framework, it's a "tool" that i wanna use to build reliable applications!
Thanks Tim and all others, for your many posts on testing, aso. But how shall i archive my goals with outdated cookbooks, with hints on test features that are in a dev-branch, with a broken release (RC3), aso ???
Shall i revert to RC2, or even better to RC1?
.... sorry i'm just frustrated .... once again.
fed up guy: Thank you for your constructive feedback ; ).
fed up guy: Hey, just talked to Nate. I will be in charge of the testing content on the cookbook from now on. Expect some good updates soon.
Gotta admit that I'm not finding unit test straightforward enough.
I'm new to cakephp, quite new to php and pretty new to programming altogether.
I am already a convert though to Test Driven Development and so the very first thing I want to do with cakephp is write a unit test and so imho this should be one of the simplest and best documented parts of the framework. In fact I'm tearing my hair out and searching high and low for advice.
So I'm looking forward to your updates to the cookbook, Tim, and please keep it simple for us newbies :)
Newbie Unit Tester: Don't. Just start using Cake and PHP and build some cool stuff. You are doing yourself exactly no favor by making live hard on yourself.
In fact I'd not recommend test driven development unless you are fairly confident in the domain you are working in. If you are new to programming you should master the basics first.
That being said, I agree with you that unit testing should be easier for beginners and good stuff is under way for that in future.
Felix: I'm doing this development as part of a Uni project and I do need to include unit test (don't give up on me yet - i'll get there!)
I may have had a bit of a breakthrough today with testing models. Previously I was getting Error: database table not found for model and I was having to create the fields as well as the records by hand in the fixture. I think I was misunderstanding the way the tests run in cakephp.
I had set up two databases: 1 for my app and 1 for test (app_test) - both defined in database.php. The app database has my tables and the app_test is empty ready for temporary test tables to be written and dropped by the unit test.
I was thinking that using:
var $import = 'User';
in my fixture would read from the default db (app). However today looking at the function init() in class CakeTestFixture, unless as connection is specified it uses the connection test_suite. So it looks like the database used for testing also has to have the tables for testing already in existence.
So now I can get my model unit test running by importing a model but also by specifying the connection either in the fixture:
var $import = array('model' => 'User', 'connection' => 'default');
or in CakeTestFixture:
$connection = isset($import['connection'])
? $import['connection']
: 'default';
So I guess I have a couple of options:
1. Go ahead with above and write the temporary test tables to my application db during testing.
2. Use my test database but make sure I have tables in place to match those in my application. I could also populate these tables with test records.
Just wondering which way round you guys do it?
Thanks!
Newbie Unit Tester: The way I prefer to do is your second options. Supply a test database, put the tables there (though empty) and use the fixtures for test data. We also use the fixtures for sample data in the real application.
Since tests run their own fixtures IMO the live database should never be used, at least not without table prefixes.
Using a separate test DB has been much smoother for us.
Tim: Thanks. I've removed my change from CakeTestFixture (after all the manual warns me about making changes to the core!) and used an sql script to create my tables in the test DB. I like your idea to keep the data in the fixtures - it keeps the data close to the test cases whilst they are being written.
However I don't understand how you "use the fixtures for sample data in the real application" - any chance you could explain?
Now I've finally got a basic model test running, it's onto the subject of this particular blog! My fixture was user_test_fixture.php. Are you recommending that fixtures be named modelname_fixture.php? If I try to do this
var $fixtures = array( 'app.user' );
(with corresponding user_fixture file in fixtures folder) i get:
Fatal error: Class 'UserFixture' not found in C:\xampp\htdocs\cake\cake\tests\lib\cake_test_case.php on line 734
Or if I stick with:
var $fixtures = array( 'app.user_test' );
(with corresponding user_test_fixture file), i get:
# Failed
Equal expectation fails as key list [0] does not match key list [0, 1] at [C:\xampp\htdocs\cake\reuser\tests\cases\models\user2.test.php line 15]
C:\xampp\htdocs\cake\reuser\tests\cases\models\\user2.test.php -> UserTestCase -> testEnabledUsers
and
Unexpected PHP error [Trying to get property of non-object] severity [E_NOTICE] in [C:\xampp\htdocs\cake\cake\libs\class_registry.php line 295]
C:\xampp\htdocs\cake\reuser\tests\cases\models\\user2.test.php -> UserTestCase -> testEnabledUsers
Any ideas what I'm doing wrong? Thanks for your help.
Newbie Unit Tester: Well I mean that we use the fixtures for the test data also as sample data for the application Have a look at our fixtures shell (just use the search feature here on the blog). Doing ./cake fixtures add the fixture data to our live db as sample data.
But as you know the fixtures are also used as test data when doing unit tests. Does that make more sense? Hereby you have *real* test data.
As for naming: Yeah it must user_fixture.php. Classname UserFixture. var $name = 'User'. The you load it via var $ifxtures = array('app.user'). In your testcase you do $this->loadFixtures(array('User'));
Please try that and tell me how it goes. : )
Tim: Started to get that feeling that I'm getting close! Thanks for the fixturize.php script. This is what i've now done:
1. Created users table including test records in the Application DB.
2. Created users table (without records) in the Test DB.
3. Ran Fixturize on the users table - creates user_fixture.php.
4. My test case looks like:
< ?php
App::import('Model', 'User');>
class UserTestCase extends CakeTestCase {
var $fixtures = array( 'app.user' );
function testEnabledUsers() {
$this->User =& ClassRegistry::init('User');
$result = $this->User->enabledUsers(array('id', 'email'));
$expected = array(
array('UserTest' => array( 'id' => 1, 'email' => 'andy@xyz.com' )),
array('UserTest' => array( 'id' => 2, 'email' => 'joe@joe.com' ))
);
$this->assertEqual($result, $expected);
}
}
?>
So I think I'm just missing your instruction on using $this->loadFixtures(array('User'));
but I can't figure out where this goes in the test case. I get a whole bunch of errors which I haven't filled your blog up with as I'm hoping I'm now making an obvious mistake which you'll spot! Thanks.
Newbie Unit Tester:
Here is a test case from the workshop application that we talked about during our workshop in Raleigh in September:
class CommandTest extends CakeTestCase {
var $fixtures = array('command', 'snippet');
function setUp() {
$this->sut = ClassRegistry::init('Command');
}
function testFindCloud() {
$this->loadFixtures('Snippet', 'Command');
$commands = $this->sut->find('cloud');
$this->assertTrue(!empty($commands));
$nestings = Set::extract('/Command/Snippet', $commands);
$this->assertIdentical(array(), $nestings);
$this->assertEqual(50, count($commands));
$commands = $this->sut->find('cloud', array('limit' => 15));
$this->assertEqual(15, count($commands));
$scaleMin = 999;
$scaleMax = 0;
foreach ($commands as $c) {
if ($c['Command']['scale'] < $scaleMin) {
$scaleMin = $c['Command']['scale'];
}
if ($c['Command']['scale'] > $scaleMax) {
$scaleMax = $c['Command']['scale'];
}
}
$this->assertEqual($scaleMin, 1);
$this->assertEqual($scaleMax, 3);
$this->sut->deleteAll(array('Command.id <>' => 0));
$commands = $this->sut->find('cloud');
$this->assertEqual(array(), $commands);
}
}
?>
Hope you can put it to use. : )
I just saw that the array('User') I told you is wrong. Sorry bout that. :S
Tim: what was wrong with array('User')?
See how I use
this->loadFixtures('Snippet', 'Command');
instead of
this->loadFixtures(array('Snippet', 'Command'));
Tim:
I'm not having a good time :(
Rather than filling up your blog comments with my code, I've posted a description of the problem at:
http://oneyeareatingcake.blogspot.com/2008/11/cakephp-unit-test-model-woes.html
If you get a chance, it'd be great if you can see what I've got wrong.
Thanks
Tim:
I've been pursuing this further via the CakePHP group:
http://groups.google.com/group/cake-php/browse_thread/thread/db65b8ed1f1f4172
I do now have a working unit test but I still couldn't get yours running without errors. It seems like everyone has a different way of writing these tests and it would be nice to see some concensus from CakePHP unit testers as to theoptimum or preferred construction of a simple model unit test that would serve as a good cookbook example to a beginner (like me)!
Thanks
Andy
Newbie Unit Tester:
Hey Andy, sorry you are not having a good time. Yeah I will work on the new Unit Testing Cookbook articles on the weekend.
Will have a look at the google group entry now.
Cheers
Tim
Thanks Tim.
I've doubled the size of cake_test_case.php & cake_test_fixture.php with debug() statements to try and understand the intended functionality! :)
In the meantime Matt Curry has given me a system which works but it imports a table into the fixture & it makes more sense to be able to import a model.
My view of how this should/could work is that:
1. I have 2 dbs - 'default' & 'test'.
2. 'default' contains tables and maybe some records for my application.
3. 'test' is an empty db - it's purpose it to have tables and data temporarily written to it during the running of tests. The tables will be truncated and dropped at the end of tests.
4. In my fixture i would prefer to specify a model and then cake uses the table definition (from 'default') to be able to create necessary tables in 'test' with a test_suite_ prefix.
5. In my fixture i'll specify my records in arrays (that could have been built by a script like your fixturize shell).
I think this is my preferred way of building a test but other options are in the fixture to define the table structure explicitly or to pull in existing records from the 'default' db via the model.
I think all of this is very close to what you have in CakePHP. But those who I have unit tests running that i have spoken to so far all have different code to get this working. I guess i'm looking for consensus on a straightforward method that works.
Thanks,
Andy
Newbie Unit Tester: Exactly, that's my view. Either import from default with a prefix or have the tables there already and just populate truncate but dont add / drop.
I hope I will find time soon to update the cookbook. ; / Glad you got a working version now.
Best
Tim
This worked for me:
http://github.com/mcurry/cakephp/tree/master/test_sample
[quote]
The differences from the baked test files:
- The fixture uses "var import" to get the schema from the original table.
- The model test doesn't need a mock model. It uses ClassRegistry::init to load the original model, but sets it to
use the test db.
[/quote]
aminalid: Yeah the same files from mcurry worked for me as well and I'm happily using it as the basis for tests I'm writing now.
I've also modified Tim & Alex's 'fixturize' script so it builds fixture files which include the "var import".
Still I'm looking forward to a 'recommended' way to write these tests & fixtures being updated into the cookbook :)
Man, as always debuggable saves the day. It took me almost whole day to figure out all the nifty bugs in my app / Cake which does not allow me to use test cases. Thank you a milion times I owe you cup of the best beer in the world (Pilsen :o))
picca: You should come to CakeFest to drink a beer with me. ; )
2 Tim: I'd really love to. But it would simply ruin my budget. Anyway as I'm always trying to pay my debts, give me a call when you're going through Pilsen.
Haha, nice. Yeah the last time I was there was around 12 years ago. :D Was there with my football team at that time for the Bohemia Cup. :]
This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.
Finally! I've been wating this for so long :)