Testing Models with CakePHP 1.2 test suite

By Mariano Iglesias (mariano)
CakePHP test suite is a powerful environment that lets you test small to large applications testing for isolated portions of your code. It is one of the coolest additions to the 1.2 release and in this article we'll see how to use it to test our application models.

Installation


First of all, you'll need to enable the test suite for your CakePHP 1.2 installation. After CakePHP is succesfully installed and configured, get the latest release of SimpleTest from its website, and uncompress it in either your cake/vendors or your app/vendors directory. You should have now a vendors/simpletest directory with all SimpleTest files and folders inside.

Make sure that you at least have a DEBUG level of 1 in your app/config/core.php file. Test your installation by running any of CakePHP core tests, pointing your browser to http://www.example.com/test.php.

About Fixtures


When testing models it is important to understand the concept of fixtures in CakePHP test suite. Fixtures are a way for you to define sample data that will be loaded in your models and will allow you to perform your testing. CakePHP uses its own settings for fixtures to not disrupt your real application data.

CakePHP will look at your app/config/database.php configuration file and test if the connection named $test is accessible. If so, it will use it to hold fixture data. Otherwise it will use the $default database configuration. On either case, it will add "test_suite" to your own table prefix (if any) to prevent collision with your existing tables.

CakePHP will perform different operations during different stages of your fixtured based test cases:

  1. Before running the first test method in your test case, it will create the tables for each of your fixtures.
  2. Before running any test method, it will optionally populate records for each of your fixtures.
  3. After running each test method, it will empty each of your fixture tables.
  4. After running your last test method, it will remove all your fixture tables.


Creating Fixtures


When creating a fixture you will mainly define two things: how the table is created (which fields are part of the table), and which records will be initially populated to the test table. Let's then create our first fixture, that will be used to test our own Article model. Create a file named article_test_fixture.php in your app/tests/fixtures directory, with the following content:

PHP Snippet:

Download code <?php 
class ArticleTestFixture extends CakeTestFixture {
    var 
$name 'ArticleTest';
    
    var 
$fields = array(
        
'id' => array('type' => 'integer''key' => 'primary'),
        
'title' => array('type' => 'string''length' => 255'null' => false),
        
'body' => 'text',
        
'published' => array('type' => 'integer''default' => '0''null' => false),
        
'created' => 'datetime',
        
'updated' => 'datetime'
    
);
    var 
$records = array(
        array (
'id' => 1'title' => 'First Article''body' => 'First Article Body''published' => '1''created' => '2007-03-18 10:39:23''updated' => '2007-03-18 10:41:31'),
        array (
'id' => 2'title' => 'Second Article''body' => 'Second Article Body''published' => '1''created' => '2007-03-18 10:41:23''updated' => '2007-03-18 10:43:31'),
        array (
'id' => 3'title' => 'Third Article''body' => 'Third Article Body''published' => '1''created' => '2007-03-18 10:43:23''updated' => '2007-03-18 10:45:31')
    );
}
?>


We use $fields to specify which fields will be part of this table, on how they are defined. The format used to define these fields is the same used in the function generateColumnSchema() defined on Cake's database engine classes (for example, on file dbo_mysql.php.) Let's see the available attributes a field can take and their meaning:

  1. type: CakePHP internal data type. Currently supported: string (maps to VARCHAR), text (maps to TEXT), integer (maps to INT), float (maps to FLOAT), datetime (maps to DATETIME), timestamp (maps to TIMESTAMP), time (maps to TIME), date (maps to DATE), and binary (maps to BLOB)
  2. key: set to primary to make the field AUTO_INCREMENT, and a PRIMARY KEY for the table.
  3. length: set to the specific length the field should take.
  4. null: set to either true (to allow NULLs) or false (to disallow NULLs)
  5. default: default value the field takes.


We lastly can set a set of records that will be populated after the test table is created. The format is fairly straight forward and needs no further explanation.

Importing table information and records



Your application may have already working models with real data associated to them, and you might decide to test your model with that data. It would be then a duplicate effort to have to define the table definition and/or records on your fixtures. Fortunately, there's a way for you to define that table definition and/or records for a particular fixture come from an existing model or an existing table.

Let's start with an example. Assuming you have a model named Article available in your application (that maps to a table named articles), change the example fixture given in the previous section (app/tests/fixtures/article_test_fixture.php) to:

PHP Snippet:

Download code <?php 
class ArticleTestFixture extends CakeTestFixture {
    var 
$name 'ArticleTest';
    var 
$import 'Article';
}
?>


This statement tells the test suite to import your table definition from the table linked to the model called Article. You can use any model available in your application. The statement above does not import records, you can do so by changing it to:

PHP Snippet:

Download code <?php 
class ArticleTestFixture extends CakeTestFixture {
    var 
$name 'ArticleTest';
    var 
$import = array('model' => 'Article''records' => true);
}
?>


If on the other hand you have a table created but no model available for it, you can specify that your import will take place by reading that table information instead. For example:

PHP Snippet:

Download code <?php 
class ArticleTestFixture extends CakeTestFixture {
    var 
$name 'ArticleTest';
    var 
$import = array('table' => 'articles');
}
?>


Will import table definition from a table called 'articles' using your CakePHP database connection named 'default'. If you want to change the connection to use just do:

PHP Snippet:

Download code <?php 
class ArticleTestFixture extends CakeTestFixture {
    var 
$name 'ArticleTest';
    var 
$import = array('table' => 'articles''connection' => 'other');
}
?>


Since it uses your CakePHP database connection, if there's any table prefix declared it will be automatically used when fetching table information. The two snippets above do not import records from the table. To force the fixture to also import its records, change it to:

PHP Snippet:

Download code <?php 
class ArticleTestFixture extends CakeTestFixture {
    var 
$name 'ArticleTest';
    var 
$import = array('table' => 'articles''records' => true);
}
?>


You can naturally import your table definition from an existing model/table, but have your records defined directly on the fixture as it was shown on previous section. For example:

PHP Snippet:

Download code <?php 
class ArticleTestFixture extends CakeTestFixture {
    var 
$name 'ArticleTest';
    var 
$import 'Article';
    
    var 
$records = array(
        array (
'id' => 1'title' => 'First Article''body' => 'First Article Body''published' => '1''created' => '2007-03-18 10:39:23''updated' => '2007-03-18 10:41:31'),
        array (
'id' => 2'title' => 'Second Article''body' => 'Second Article Body''published' => '1''created' => '2007-03-18 10:41:23''updated' => '2007-03-18 10:43:31'),
        array (
'id' => 3'title' => 'Third Article''body' => 'Third Article Body''published' => '1''created' => '2007-03-18 10:43:23''updated' => '2007-03-18 10:45:31')
    );
}
?>


Creating your test case


Let's say we already have our Article model defined on app/models/article.php, which looks like this:

Model Class:

Download code <?php 
class Article extends AppModel {
    var 
$name 'Article';
    
    function 
published($fields null) {
        
$conditions = array(
            
$this->name '.published' => 1
        
);
        
        return 
$this->findAll($conditions$fields);
    }

}
?>


We now want to set up a test that will use this model definition, but through fixtures, to test some functionality in the model. CakePHP test suite loads a very minimum set of files (to keep tests isolated), so we have to start by loading our parent model (in this case the Article model which we already defined), and then inform the test suite that we want to test this model by specifying which DB configuration it should use. CakePHP test suite enables a DB configuration named test_suite that is used for all models that rely on fixtures. Setting $useDbConfig to this configuration will let CakePHP know that this model uses the test suite database connection.

Since we also want to reuse all our existing model code we will create a test model that will extend from Article, set $useDbConfig and $name appropiately. Let's now create a file named article.test.php in your app/tests/cases/models directory, with the following contents:

PHP Snippet:

Download code <?php 
loadModel
('Article');

class 
ArticleTest extends Article {
    var 
$name 'ArticleTest';
    var 
$useDbConfig 'test_suite';
}

class 
ArticleTestCase extends CakeTestCase {
    var 
$fixtures = array( 'article_test' );
}
?>


As you can see we're not really adding any test methods yet, we have just defined our ArticleTest model (that inherits from Article), and created the ArticleTestCase. In variable $fixtures we define the set of fixtures that we'll use.

Creating our first test method


Let's now add a method to test the function published() in the Article model. Edit the file app/tests/cases/models/article.test.php so it now looks like this:

PHP Snippet:

Download code <?php 
loadModel
('Article');

class 
ArticleTest extends Article {
    var 
$name 'ArticleTest';
    var 
$useDbConfig 'test_suite';
}

class 
ArticleTestCase extends CakeTestCase {
    var 
$fixtures = array( '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);
    }
}
?>


You can see we have added a method called testPublished(). We start by creating an instance of our fixture based ArticleTest model, and then run our published() method. In $expected we set what we expect should be the proper result (that we know since we have defined which records are initally populated to the article_tests table.) We test that the result equals our expectation by using the assertEqual method.

Running your test


Make sure that you at least have a DEBUG level of 1 in your app/config/core.php file, and then point your browser to http://www.example.com/test.php. Click on App Test Cases and find the link to your models/article.test.php. Click on that link.

If everything works as expected, you will see a nice green screen saying that your test succeded.

 

Comments 324

CakePHP Team Comments Author Comments
 

Question

1 Duplicated Fields in Fixture

Is it really necessary to specify the database table $fields in the fixture? This seems like duplicated effort and a real time sink for testing. Can Cake just use the existing table?
Posted Apr 7, 2007 by Zach Cox
 

Comment

2 Duplicated Fields in Fixture

@Zach: if you read the article when I talk about fixtures I mention the intention of not manipulating nor disrupting your application data. Also, your fixtures could be running on a completely separate database than your application. The idea is that you can even test your models with new fields that you have not yet added to your application, thus fixtures not only specify which records are initially populated, but also how the table is built.
Posted Apr 7, 2007 by Mariano Iglesias
 

Comment

3 Duplicated Fields in Fixture

Thanks for the reply Mariano - that makes things more clear. If I do just want to use exactly the same structure as my existing table, will Cake just use that if I don't declare that $fields variable?

Also, will you please write more great tutorials about unit testing behaviors, components, controllers, helpers, and groups? :)
Posted Apr 7, 2007 by Zach Cox
 

Comment

4 Duplicated Fields in Fixture

@Zach: Not, leaving $fields out of the fixture definition will generate an error. However, post an enhancement ticket for 1.2 on the trac at https://trac.cakephp.org and I'll consider adding such functionality.

About writing more tutorials, I'll try to make time for it :)
Posted Apr 7, 2007 by Mariano Iglesias
 

Question

5 Relations

This is great, you guys have put in so much work!

How would you test relations on the models? Just testing a single model is bit too simplified in real world cases.
Posted Apr 8, 2007 by Mladen Mihajlovic
 

Comment

6 Relations

@Mladen: I recommend you look at cake/tests/cases/libs/model/model.test.php, its fixtures are located at cake/tests/fixtures. You will see *A LOT* of models being tested, most of them with several relations.
Posted Apr 8, 2007 by Mariano Iglesias
 

Question

7 Funny Fixtures

Hi Mariano,

First of all can I say that I am excited that testing is now being integrated with CakePHP.
However, there are a few things I'd like to ask about your testing framework.

1.
First of all, why the fields array? This means that I have to duplicate my database schema in my SQL file and my tests. Doesn't this go against the DRY principal?
I do like the fact that the tables are recreated each time but wouldn't it be better to create them from the SQL schema file for the application in question rather than repeat it in the fixture files?
If I make a change to my database e.g. PhpMyAdmin. I can go and make this change to my test fixtures (or let the fixture import it from the model) but forget to export the change to the schema file under version control in my repository. Then, code that I commit, passing all tests and therefore assumed to be working, may in fact crash when someone checks it out of the repository because the schema file may have inconsistencies with the schema used in the code.
This is something I find I do all the time, maybe it's just me.

2.
While asking questions about testing with a real database, Marcus Baker on (the creator of SimpleTest) replied suggesting the use of a separate database connection for the schema creation and the tests themselves. Although I can't quite remember why. Are you doing this?

3.
As you've set written the array of expected values in the test itself, it isn't clear whether the fixture data in the tests to dynamically test the results, as in $this->article_test->records[0][id]
Is this possible?

4.
It looks like this test suite has a lot in common with Dhofstets testing suite. Why did you guys decide to roll your own rather than work with this existing one.

I've been hoping for some good testing support for cake for a good while now. As such I'm very excited about what's happening at the moment. Thanks for this. I hope my questions seem valid.

Cheers,

Sonic

Posted Apr 23, 2007 by Sonic Baker
 

Comment

8 Funny Fixtures

@Sonic:

1. Please read the article entirely. Look for section "Importing table information and records" and you'll see that you can import your current table schema from existing models/tables.

2. Again, read the article, particularly section "About Fixtures". You can set up your $test connection with persistent set to false and that's exactly what you will achieve.

3. It is up to you what you expect and how you do the matching. You can expect exact records, or dynamically generated records. For example your test could be against a findCount(), and you could $this->assertTrue($result > 5);

4. It was exactly the opposite way. Larry asked a long time ago to dhofstet to contribute to Cake's built in Test Suite, but instead he rolled out his own. Nothing good or bad, just the way it happened.
Posted Apr 23, 2007 by Mariano Iglesias
 

Bug

9 typo

Hi, I'm really excited to see Cake intigrate unit tests. Great work. Thanks for the excellent article explaining everything too.

Small bug on the page, the tests dir is plural. You refer to it in paths as singular in few places. For example - (app/test/fixtures/article_test_fixture.php)

Also, I'm having a bugger of a time getting fixtures to load up. I've followed the examples almost exactly, but still get this in the test output - "No Database table for model UserTest (expected test_suite_user_tests), create it first."

Any Chance things are still a bit buggy in 1.2.0.4798alpha?
Posted Apr 24, 2007 by mattclark
 

Comment

10 typo

@mattclark: Typo corrected, thanks for the heads up. Always update to the latest SVN head when using CakePHP 1.2 since it is still on alpha stage. Try that and let me know.
Posted Apr 24, 2007 by Mariano Iglesias
 

Comment

11 Funny Fixtures

@Mariano:
Please re-read my question.

1.
you can import your current table schema from existing models/tables.

You will see that I am aware that I can import the schema from the Model/Database but I am talking about importing from my exported database schema file. When a user checks my application out of the repository, they will not have an existing database for the application. They must import it from the included schema file included with the application. This is the file I am talking about with respect to creating the database schema for the tests.
If I make a change to the database with a client such as PhpMyAdmin, then these changes will only exist in the live database until I export the changes to my schema file and check it into the repository.
If I forget to export these changes to my schema file (which I regularly do), my tests will still pass if I set the fixtures to get the schema from the existing database/Model. However, once checked into the repository, these tests won't pass with the database schema in the repository.

2.
You can set up your $test connection with persistent set to false and that's exactly what you will achieve.
Ah, cool. Thanks.

3.
$this->assertTrue($result > 5);
It is the '5' here that I'm talking about. I was wondering if it is necessary to hard code this number. If I change this value in the Fixture file, will I also have to change it in the test file also? Or, is it possible to reference a particular record from the fixture file in the test files? This way the data will only have to be changed in one place.

4.
Nothing good or bad, just the way it happened.
Cool, just wondering.

By the way, great article. I was just a bit unclear about the above.

Cheers,

Sonic
Posted Apr 25, 2007 by Sonic Baker
 

Comment

12 mysterious missing table

@mattclark: Typo corrected, thanks for the heads up. Always update to the latest SVN head when using CakePHP 1.2 since it is still on alpha stage. Try that and let me know.

Found the problem. Not sure exactly what to do about it though. Here's what's going on--

All of the fixtures are loding up fine, it is actually in the model I'm testing that I get into trouble. With the current way testing is set up, you can not create new objects in a model, and have them saved to the testing db.

So, I have a setup like so --

class RealClass extends AppModel
{
function doStuff()
{
$rc = new RealClass();
$rc->save();
}
}


Then, in my test, set everything up just like you outline above. That new RealClass object is going to be saved in the default db. This code is untestable.

bummer.

matt
Posted Apr 28, 2007 by mattclark
 

Comment

13 fix for mysterious missing table

Here is my fix for my issue with testing the creation of new objects from within a model. (I also posted it on Trac).

This may not work for everyone, as it assumes that you don't have any tables with the same name in separate db's. But, I suspect that is most people :). basically all I did was remove the 'test_suite_' prefix for table names, which seems redundant to me, and removed the ability to test in the same db as 'default', which seems like a bad idea anyways.

If you want to do this, have all your test classes inherit from this class, and apply the diff below to your /app/cake directory. Then name your fixtures the same thing as your Model names.

<?php

class TestCase extends CakeTestCase
{

function before($method)
{
if (!defined('ENVIRONMENT')){
define("ENVIRONMENT", "testing");
}
parent::before($method);
}

}

?>



Here is the diff ----------------



ndex: tests/lib/cake_test_case.php
===================================================================
--- tests/lib/cake_test_case.php (revision 4896)
+++ tests/lib/cake_test_case.php (working copy)
@@ -57,17 +57,8 @@
@$db =& ConnectionManager::getDataSource('test');
set_error_handler('simpleTestErrorHandler');

- // Try for default DB
- if (!$db->isConnected()) {
- $db =& ConnectionManager::getDataSource('default');
- }
-
- // Add test prefix
- $config = $db->config;
- $config['prefix'] .= 'test_suite_';
-
// Set up db connection
- ConnectionManager::create('test_suite', $config);
+ ConnectionManager::create('test_suite', $db->config);

// Get db connection
$this->db =& ConnectionManager::getDataSource('test_suite');
Index: libs/model/connection_manager.php
===================================================================
--- libs/model/connection_manager.php (revision 4890)
+++ libs/model/connection_manager.php (working copy)
@@ -93,8 +93,11 @@
* @return object
*/
function &getDataSource($name) {
+ if(defined('ENVIRONMENT') && ENVIRONMENT == 'testing'){
+ $name = 'test';
+ }
+
$_this =& ConnectionManager::getInstance();
-
if (in_array($name, array_keys($_this->_dataSources))) {
return $_this->_dataSources[$name];
}






Posted Apr 29, 2007 by mattclark
 

Question

14 Missing Database Table

Mariano,

Thanks for writing such an essential piece of the documentation on how to use the tests.

However, I'm getting Missing Database Table for my *Test class. I'm try to adapt your examples to a test for the TournamentGame model, so my derived class should be this, right?:


loadModel('TournamentGame');

class TournamentGameTest extends TournamentGame {
    var $name = 'TournamentGameTest';
    var $useDbConfig = 'test_suite';



But I get this error:


Missing Database Table

No Database table for model TournamentGameTest (expected test_suite_tournament_game_tests), create it first.

Notice: If you want to customize this error message, create app\views\errors\missing_table.ctp


I was under the impression from your article that CakePHP automagically knows that the *Test derived class corresponds to the model, but perhaps I'm misunderstanding something, or perhaps it's changed in recent SVN.

I'm on revision 6123, by the way.
Posted Dec 6, 2007 by Philip Reed
 

Comment

15 I found one of my problems

I had accidentally named tournament_game_test_fixture.php in the plural, tournament_**games**_test_fixture.php . I'm still not getting fixtures working 100%, but at least I'm more on track now.
Posted Dec 19, 2007 by Philip Reed
 

Comment

16 loadModel() does not works..

Hello All,

I have try to test my modules but it gives error that

Fatal error: Class 'Object' not found in C:\\xampp\\htdocs\\sch\\cake\\libs\\model\\datasources\\datasource.php on line 37

my testing location is C:\\xampp\\htdocs\\sch\\app\\tests\\app\\cases\\models

Could any body help me for this.

Regards,

Posted Feb 7, 2008 by jach
 

Comment

17 loadModel depricated

I have try to test my modules but it gives error that

Fatal error: Class 'Object' not found in C:\\xampp\\htdocs\\sch\\cake\\libs\\model\\datasources\\datasource.php on line 37


loadModel() is depricated.

Try App::import('Model', 'foo');
Posted Feb 11, 2008 by Nate Todd
 

Question

18 Test Suite not creating test tables

Hello Mariano, thanks for the tutorial.

Here's my issue, I created a simple silly test to practice, I think I follow all the instructions you give, but the suite does not seem to be creating the test tables.

I get this error:

Error:  Database table test_suite_albums for model Album was not found.


And I'm using this fixture (I tried specifying the table definition as well with no luck):

<?php 
class AlbumTestFixture extends CakeTestFixture {
    var 
$name 'AlbumTest';
    var 
$import 'Album';
    
    var 
$records = array(
        array (
'id' => 1'artist_id' => '1''name' => 'Album 1 Name''year' => '1998'),
        array (
'id' => 2'artist_id' => '2''name' => 'Album 2 Name''year' => '1999'),
        array (
'id' => 3'artist_id' => '3''name' => 'Album 3 Name''year' => '2001'),
        array (
'id' => 4'artist_id' => '4''name' => 'Album 4 Name''year' => '2005')
    );
}
?> 


Please help!
Posted Mar 14, 2008 by Luis Molina
 

Bug

19 Problem with fixtures with a has and belongs to many relation

Hi,

we tried to use this and all went along fine until testing our models that had a has and belongs to many relation (HABTM).
Our setup: we used a prefix for the test tables, that were in the same database.
Database tables: posts tags posts_tags tests_posts test_tags test_posts_tags
(a post can have multiple tags, tags can belong to many posts)

The behaviour was the following:
- when working just with data from tags or posts, the testing infrastructure queried, correctly, tests_tags or tests_posts
- when querying something that involved the relation, the query did not contain the test prefix for the join, so it
queried
tests_tags
tests_posts
and attempted to do the join using posts_tags instead of tests_posts_tags

As after some digging through the code we did not manage to find it and fix it, we switched to this implementation http://cakebaker.42dh.com/2006/12/18/testing-with-cakephp-12-a-preview/[url] that we could use with better time-to-workingtest. :)

Sorry for not having the time to provide the actual code and sql :|
Posted Apr 12, 2008 by Irina Dumitrascu
 

Bug

20 PS

I forgot to mention that we used importing table information in the beginning, then gave it a try with structure and data specification arrays - both did not work.
Posted Apr 12, 2008 by Irina Dumitrascu
 

Bug

21 Class Not found error

I have followed the steps you mentioned above. But still I am gettng 'Fatal error: Class 'User' not found in /tests/cases/models/user.test.php' I am putting my user.test.php's code here. Please let me know what i am missing.

<?php
App::import('User','User');

class UserTest extends User{
var $name = 'UserTest ';
var $useDbConfig = 'test_suite';

}

class UserTest Case extends CakeTestCase {
var $fixtures = array( 'user_test' );
function testInactive() {
$this->UserTest =& new UserTest ();

$result = $this->UserTest ->inactive(array('id', 'name'));
$expected = array(
array('UserTest ' => array( 'id' => 1, 'name' => 'User Communities' ))

);

$this->assertEqual($result, $expected);
}
}
?>


PLease reply. Waiting for your reply.
Posted Apr 17, 2008 by Bhushan
 

Comment

22 Limit the no of records imported

I find the import pretty useful but is there a way to limit the no. of records imported? In this case, we might have too many records yet it'd be useful to just have some of them being used during tests.
Posted May 25, 2008 by Derick