Unit Testing Models with Phactory and PHPUnit post

Comments

PHP Database Unit Testing with Phactory

PHPUnit, although exceptional in so many aspects, is somewhat lacking when it comes to database testing. Testing models in with PHPUnit usually requires you to set up cumbersome XML files for fixtures and your are stuck with them for the entire testcase. I am going to show you how to unit test your models quickly and easily with an awesome PHP database testing tool called Phactory.

Phactory is an alternative to using fixtures for testing code that interacts with a database. What this means is that instead of building a fixture with xml and using it for an entire test case (series of tests), we can manipulate our test database directly on a per-test level. This allows use to quickly create only the records we need for a test, and remove them from the database after each test automatically.

Installing Phactory Phactory is available as a PEAR package and is super easy to install:

pear channel-discover pearhub.org
pear install pearhub/Phactory

Once Phactory is installed, you want to integrate Phactory with your test runner. You should probably create a new base test case for models using Phactory. Let's do that now:

/*
 * /models/shoe.php
 */

require_once 'PHPUnit/Autoload.php';
require_once 'Phactory/lib/Phactory.php';

/**
 * Test Case Base Class for using Phactory *
 */
abstract class PhactoryTestCase extends PHPUnit_Framework_TestCase
{
  protected static $db;

  public static function setUpBeforeClass()
  {
    //set up Phactory db connection
    self::$db = self::getConnection();
    Phactory::reset();
  }

  public static function tearDownAfterClass()
  {
    Phactory::reset();
  }

  protected function setUp()
  {
    Phactory::reset();
  }

  protected function tearDown()
  {
    Phactory::reset();
  }

  /**
   * Sets up Phactory PDO connect
   */
  public static function getConnection()
  {
    $options = array (
      'driver'   => 'mysql',
      'host'     => '127.0.0.1',
      'user'     => 'root',
      'password' => '',
      'database' => 'project_test_db'
    );

    $pdo = new PDO($options['driver'].':host='.$options['host'].';dbname='.$options['database'], $options['user'], $options['password']);
    Phactory::setConnection($pdo);
  }
}

This abstract base class, PhactoryTestCasewill be extended by our model class. In PhactoryTestCase::setUpBeforeClass(), which runs before all the tests in a given test case, we create the connection to the test database. We also want to ensure that any records are truncated in between individual tests. By calling Phactory::reset() I have ensured that all records and Phactory table definitions (blueprints and inflections) will be cleared.

Setting Up the Test Database Your test database should be a copy of your project's main database, minus any records. A quick way to get a SQL dump of a database without any records is:

mysqldump --host="localhost" --user="root" project_db --no-data > "schema.sql";

Substitute the values for your project of course. What I am doing here is dumping the schema for the main database

project_db into a sql file which I will use to create my test database. Go ahead and execute that sql on your test database, but be aware that you will have to keep any schema changes between the two databases in sync.

Setting Up a Model to Test

The model I am going to test today is called Shoe.

<?php
/*
 * /tests/PhactoryTestCase.php
 */
class Shoe
{
  public function __construct($pdo)
  {
    $this->db = $pdo;
  }

  public function getShoesByBrandName($brand_name = '')
  {
    if ($brand_name) {
      $sql   = "SELECT * FROM shoe_table WHERE brand_name = '{$brand_name}'";
      $shoes = array();

      foreach ($this->db->query($sql) as $row) {
        $shoes[] = $row;
      }

      return $shoes;
    }
  }
}

In this simple model, we are going to use a constructor supplied PDO object to act as the database. In fact, we are going to use the same PDO object that Phactory is using and just pass it in during our tests. In production of course you would be talking to your main database, but we need to be able to swap in our test db in order to test it. In practice, you will probably already have a database abstraction layer. The trick in that case would be to switch to the test database via a bootstrap,  or inject the test database config to your abstraction layer via your test runner.

The model has one method, Shoe::getShoesByBrandName() which takes a string, the brand name. It executes some SQL and returns an array of shoes. Let's go ahead and set up a test case for this model.

<?php
/*
 * /tests/models/ShoeTest.php
 */

require_once '/models/shoe.php';
require_once 'PhactoryTestCase.php';

class ShoeTest extends PhactoryTestCase
{
  protected $sut;

  protected function setUp()
  {
    parent::setUp();
    $this->sut = new Shoe(parent::$db);

    //make sure Phactory doesn't try to pluralize table name
    Phactory::setInflection('shoe_table', 'shoe_table');

    //create empty Phactory blueprint
    Phactory::define('shoe_table', array());
  }
}

Here is a basic test case for our Shoe model. In the ShoeTest::setUp() we call the methods hanging out in parent::setUp(), which includes setting up the connection to the database. Then I populate ShoeTest::sut with a new instance of our shoe model object, passing it the pdo object stored in PhactoryTestCase::$db.

We also sometimes need to tell Phactory not to pluralize our table names when it does it's magic by calling Phactory::setInflection().

A blueprint must be defined for any tables we want to use in our test. We do this by calling Phactory::define(). The first argument is the name of the table, the second argument is an array of default values that will be populated upon an insert. In this array you specify keys matching column names in your table with a default value. For now I am passing in an empty array because I don't want to set up any default values.

Next, we create a method to test Shoe::getShoesByBrandName() w/ no args.

<?php
public function testGetShoesByBrandName()
{
  $actual = $this->sut->getShoesByBrandName();
  $this->assertNull($actual);
}

This is usually always the first test I write since we should get a null value when passing no arguments to our method. Now for a real test.

<?php
public function testGetShoesByBrandName1()
{
  //should only pull up Nike Shoes
  Phactory::create('shoe_table', array('id' => 123, 'color' => 'blue', 'brand_name' => 'Nike'));
  Phactory::create('shoe_table', array('id' => 234, 'color' => 'red',  'brand_name' => 'Nike'));</p>

  //should not pull up
  Phactory::create('shoe_table', array('id' => 345, 'color' => 'blue', 'brand_name' => 'Vans'));
  Phactory::create('shoe_table', array('id' => 456, 'color' => 'blue', 'brand_name' => 'Reebok'));
  Phactory::create('shoe_table', array('id' => 567, 'color' => 'blue', 'brand_name' => 'New Balance'));

  $actual = $this->sut->getShoesByBrandName('Nike');

  //should pull up first two rows
  $this->assertEquals(2, count($actual));
}

Here I use Phactory::create() to build 5 records for my test. When this test runs, Phactory will insert all of these rows into the test database. I then call Shoe::getShoesByBrandName() and expect to get back the two records that are Nike shoes. I assert that two rows have been pulled up by the method.

That Was Easy

You can also do more complicated multi-table relationships and joins in your Model code, and all you have to do in your test case is create a corresponding table blueprint. You will also need to create any records you need in all of the tables you are testing. This offers you incredible flexibility as opposed to developing with fixtures since you can control what records you need when you need them. You also don't need to specify every column when defining a table blueprint so you will save a ton of time over using a PHPUnit XML dataset.

Hopefully this post has given you incentive to take a look at Phactory. They have a very well written usage guide that goes into quite a bit more depth than the code I have presented. Make sure you check out the Phactory Homepage and Phactory on GitHub. I also want you to watch this video overview of Phactory that features the author walking through several features of the component. Thanks for reading!

  • Tags:
  • database testing
  • database unit testing
  • dbunit
  • phactory
  • php
  • phpunit
  • phpunit database testing
  • testing models

explosive web programming MODERN CODE TACTICS

by James Fuller