Creating fixtures and testing model methods

In this recipe, we will learn how to create test data that we can use to test our application without altering real data, and how to create our own unit tests to cover model functionality.

Getting ready

To go through this recipe, we need a basic application skeleton to work with and have the SimpleTest library installed. Go through the entire recipe, Setting up the test framework.

How to do it...

  1. Create a file named article_fixture.php and place it in your app/tests/fixtures folder with the following contents:
    <?php
    class ArticleFixture extends CakeTestFixture {
    public $import = 'Article';
    public $records = array(
    array(
    'id' => 1,
    'title' => 'Article 1',
    'body' => 'Body for Article 1'
    ),
    array(
    'id' => 2,
    'title' => 'Article 2',
    'body' => 'Body for Article 2'
    )
    );
    }
    ?>
    
  2. Create a file named user_fixture.php and place it in your app/tests/fixtures folder with the following contents:
    <?php
    class UserFixture extends CakeTestFixture {
    public $table = 'users';
    public $import = array('table' => 'users'),
    public $records = array(
    array(
    'id' => 1,
    'username' => 'john.doe'
    ),
    array(
    'id' => 2,
    'username' => 'jane.doe'
    ),
    array(
    'id' => 3,
    'username' => 'mark.doe'
    )
    );
    }
    ?>
    
  3. Create a file named vote_fixture.php and place it in your app/tests/fixtures folder, with the following contents:
    <?php
    class VoteFixture extends CakeTestFixture {
    public $import = 'Vote';
    public $records = array(
    array(
    'article_id' => 1,
    'user_id' => 1,
    'vote' => 4
    ),
    array(
    'article_id' => 1,
    'user_id' => 3,
    'vote' => 5
    ),
    array(
    'article_id' => 1,
    'user_id' => 2,
    'vote' => 4
    ),
    array(
    'article_id' => 2,
    'user_id' => 2,
    'vote' => 3
    ),
    array(
    'article_id' => 2,
    'user_id' => 3,
    'vote' => 4
    )
    );
    }
    ?>
    
  4. Create a file named article.test.php and place it in your app/tests/cases/models folder with the following contents:
    <?php
    class ArticleTestCase extends CakeTestCase {
    public $fixtures = array('app.article', 'app.user', 'app.vote'),
    public function startTest($method) {
    parent::startTest($method);
    $this->Article = ClassRegistry::init('Article'),
    }
    public function endTest($method) {
    parent::endTest($method);
    ClassRegistry::flush();
    }
    public function testGet() {
    $article = $this->Article->get(1);
    $this->assertTrue(!empty($article) && !empty($article['Article']));
    $this->assertTrue(!empty($article[0]) && !empty($article[0]['vote']));
    $this->assertEqual(number_format($article[0]['vote'], 1), 4.3);
    $article = $this->Article->get(2);
    $this->assertTrue(!empty($article) && !empty($article['Article']));
    $this->assertTrue(!empty($article[0]) && !empty($article[0]['vote']));
    $this->assertEqual(number_format($article[0]['vote'], 1), 3.5);
    }
    public function testVote() {
    $result = $this->Article->vote(2, array('Vote' => array(
    'user_id' => 2
    )));
    $this->assertFalse($result);
    $this->assertTrue(!empty($this->Article->Vote->validationErrors['vote']));
    $result = $this->Article->vote(2, array('Vote' => array(
    'user_id' => 2,
    'vote' => 6
    )));
    $this->assertFalse($result);
    $this->assertEqual($this->Article->Vote->validationErrors['vote'], 'range'),
    $result = $this->Article->vote(2, array('Vote' => array(
    'user_id' => 2,
    'vote' => 1
    )));
    $this->assertFalse($result);
    $result = $this->Article->vote(2, array('Vote' => array(
    $result = $this->Article->vote(2, array('Vote' => array(
    'user_id' => 1,
    'vote' => 1
    )));
    $this->assertTrue($result);
    $article = $this->Article->get(2);
    $this->assertTrue(!empty($article[0]) && !empty($article[0]['vote']));
    $this->assertEqual(number_format($article[0]['vote'], 1), 2.7);
    $this->expectException();
    $this->Article->vote(2);
    }
    }
    ?>
    

How it works...

When looking to test model methods, it is very important to know what data is used during testing. Even when it is perfectly possible to test models using real application data, it is often safer (and thus recommendable) to specify the data that will be used for testing. This way, any modification to real data should not affect our tests, and consequently running those tests should not affect real data.

For this very purpose, CakePHP offers the concept of fixtures, which are no more than PHP classes that define the table structure and data used for testing models. These fixtures should have the same name as the model they are providing data for, should extend the base class CakeTestFixture, and should end with the word Fixture. The file name should be the underscored version of the class name, and should be placed in the app/tests/fixtures directory. A fixture may define the following properties:

  • name: The name of the fixture, used to determine the name of the table this fixture creates. If the table name can be determined by other means, such as by setting the table property, or by importing the structure from a model, then this property is optional.
  • table: The table this fixture creates. If the fixture imports the structure from an existing model, or if the name property is specified, then this property is optional.
  • import: This property is optional and allows the structure, and/or data, to be imported from an existing source. If this property is set to a string, then it is a model name from where to import the structure (not the records.) Otherwise, it should be an array that consists of the following settings:
    • records: An optional Boolean setting. If set to true, then all records will be imported from the specified source. Defaults to false.
    • model: The model from where to import the structure, and/or data. If specified, this model must exist.
    • table: The table from where to import the structure, and/or data. If the model setting is specified, this setting is ignored and thus is optional.
    • fields: If import is not defined, then this property is mandatory. It should be an array where each key is a field name, and each value the definition of the field, containing settings such as: type, length, null, default, and key. For more information about these settings, see http://book.cakephp.org/view/1203/Creating-fixtures.
    • records: An array of records, each record itself being an array where the keys are the field names, and the values their respective values.

We start by creating the following fixtures:

  • ArticleFixture: It imports its structure from the Article model, and defines two records.
  • UserFixture: It imports its structure from the users table and defines three records (Notice how we import from a table instead of a model, as we did not create a User model).
  • VoteFixture: It imports its structure from the Vote model, and defines five records.

After creating the fixtures, we proceed to build the test case. A test case is a PHP class without naming restrictions that contains unit tests. It extends from CakeTestCase, and is saved in a file ending with the suffix .test.php and placed in an appropriate subdirectory of the app/tests/cases folder. A unit test is a method of a test case class, but only methods with names starting with the word test are considered unit tests and thus run when the test case is executed.

Our test case is named ArticleTestCase, and defines the fixtures property to specify which fixtures are utilized by the test case. These names should match the fixture file name, without the _fixture.php suffix. By means of these fixtures, we provide test data for the models used throughout our test case.

Whenever you instantiate models from a unit test, and unless you specify otherwise through settings sent to the ClassRegistry::init() method, CakePHP will automatically set the model's database configuration to be test_suite, not only for the directly instantiated models, but for any models instantiated as a result of a binding definition.

The test_suite database configuration, unless specifically changed by the developer, will use the same database configuration as defined in the default configuration, and will also set test_suite_ as a table prefix to avoid overwriting existing tables. This means that any models that are instantiated, together with their bindings (including bindings of bindings, and so on) should have a matching fixture, and those fixtures should be added to the test case. If you want to avoid defining fixtures for models you do not intend to test, see the section Extending models to avoid testing unneeded bindings in this recipe.

The first two methods in ArticleTestCase are implementations of callbacks offered by the parent class CakeTestCase. There are four callbacks available:

  • startCase(): It executed before the first unit test method is run. This method is executed once per test case.
  • endCase(): It executed after the last unit test method was run. This method is executed once per test case.
  • startTest(): It executed before each unit test method is run. It receives a single argument, which is the name of the test method that is about to be executed.
  • endTest(): It executed after each unit test method was run. It receives a single argument, which is the name of the test method.

We use the startTest() callback to instantiate the model we intend to test (Article in this case), and the endTest() callback to clean up the registry, a step that is not needed for this particular test case but that serves useful in many other scenarios.

We then define two unit test methods: testGet() and testVote(). The first one is meant to provide testing for the Article::get() method, while the later tests the creation of votes through the Article::vote() method. In these tests, we issue different calls to the model method we are testing, and then use some of the test case assertion methods to evaluate these calls:

  • assertTrue(): Asserts that the provided argument evaluates to true.
  • assertFalse(): Asserts that the provided argument evaluates to false.
  • assertEqual(): Asserts that the first argument is equal to the second argument.
  • expectException(): Expects the next call to produce an exception. Because of the way exceptions are handled, this assertion should be made last in the test method, as any code within that unit test method that should be executed after the exception is thrown will be ignored. Another approach to avoid this limitation is to use a try-catch block, and manually issue a call to the fail() or pass() method as a result.

There are other assertion methods that are useful in other scenarios, such as:

  • assertIsA(): Asserts that the first argument is an object of the type provided in the second argument.
  • assertNull(): Asserts that the provided argument is null.
  • assertPattern(): Asserts that the second argument matches the regular expression pattern defined in the first argument.
  • assertTags(): Asserts that the first argument matches the HTML tags provided in the second argument, without consideration to the order of tag attributes. See recipe Testing views for an example use of this assertion method.

There's more...

This recipe has shown us how to easily create fixtures. However, when there are lots of models in our application this can become quite a tedious task. Fortunately, CakePHP's bake command offers a task to automatically create fixtures: fixture.

It can run in interactive mode where its questions guide us through the steps required, or by using command line parameters. If we wanted to create a fixture for our Article model with up to two records, we would do:

On a GNU Linux / Mac / Unix system:

../cake/console/cake bake fixture article -count 2

On Microsoft Windows:

..cakeconsolecake.bat fixture article -count 2

This would generate the article_fixture.php file in its correct location, with two sample records ready to be used.

Extending models to avoid testing unneeded bindings

In this recipe, we tested code that affects the Article and Vote models, but none of the functionality that was covered by these unit tests had to interact with the User model. Why did we then need to add the user fixture? Simply removing this fixture from the fixtures property will make CakePHP complain about a missing table (specifically, test_suite_users).

To avoid creating fixtures for models we are not testing, we can create modified versions of our model classes by extending them and re-defining their bindings, leaving in only those we intend to test. Let us modify our test case to avoid using the user fixture.

Add the following to the beginning of your app/tests/cases/models/article.test.php file:

App::import('Model', array('Article', 'Vote'));
class TestArticle extends Article {
public $belongsTo = array();
public $hasOne = array();
public $hasMany = array(
'Vote' => array('className' => 'TestVote')
);
public $hasAndBelongsToMany = array();
public $alias = 'Article';
public $useTable = 'articles';
public $useDbConfig = 'test_suite';
}
class TestVote extends Vote {
public $belongsTo = array();
public $hasOne = array();
public $hasMany = array();
public $hasAndBelongsToMany = array();
public $alias = 'Vote';
public $useTable = 'votes';
public $useDbConfig = 'test_suite';
}

While still editing the article.test.php file, change the fixtures property of the ArticleTestCase class so that the user fixture is no longer loaded:

public $fixtures = array('app.article', 'app.vote'),

Finally, change the instantiation of the Article model so that it uses TestArticle instead, by making the following changes to the startTest() method of the ArticleTestCase class:

public function startTest($method)
{
parent::startTest($method);
$this->Article = ClassRegistry::init('TestArticle'),
}

Analyzing code coverage

If you have Xdebug installed (information about it is available at http://xdebug.org) you can find out how much of your application code is covered by your unit tests. This information is a great tool for understanding which parts of your application need more testing.

Once you have run a test case, you will notice a link entitled Analyze Code Coverage. After running our test case, click on this link. CakePHP will inform us that we have fully covered (100% coverage) our code. If you now comment out the unit test method called testVote(), and then run the code coverage analysis, you will notice that this number drops to 47.62%, and CakePHP also shows us which part of our code has not been covered by unit tests, as shown in the next screenshot:

Analyzing code coverage

When you achieve 100% code coverage, you are not guaranteeing that your code is bug-free, but that all lines of your application code have been reached by at least one unit test.

The more code left out of the reach of unit tests, the more prone to bugs your application becomes.

See also

  • Testing controller actions and their views
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.251.56