Tuesday, November 13, 2012

Learning lessons at ZendUncon

In my previous post I already mentioned Sebastian Jerzy WilczyƄski (@dj_sebastian_w) and his uncon session "Unit Testing for Databases using fixtures and phpunit". It made me wonder how much difference it would make if you use fixtures instead of testing database interactions using predefined sets of data.

Since I work a lot with Zend Framework I was looking at how I could use fixtures and mock objects to follow Sebastian's guidelines. So I gave it a try. This is what I came up with.

I use the domain model design pattern for accessing data storages with Zend Framework so this means I have a model (Order_Model_Order) which is the representation of my data object, a mapper (Order_Model_OrderMapper) to connect my model with an underlying data storage backend and a table gateway object (Order_Model_DbTable_Order) to link everything to the database. The object we're going to focus on is Order_Model_OrderMapper as this is the link that connects my model to the database.

At first I start my test class with fixtures.

<?php

class Order_Model_OrderMapperTest extends PHPUnit_Framework_TestCase
{
    protected $_order = array (
        0 => array (
            'id'             => 1,
            'status'         => 0,
            'price'          => 2499.95,
            'currency'       => 'USD',
            'exchange_rate'  => 1.2714,
            'vat_id'         => 3,
            'vat_percentage' => 0.00,
            'created_by'     => 400,
            'created_at'     => '2012-02-29 10:11:12',
            'updated_by'     => 303,
            'updated_at'     => '2012-11-04 03:01:01',
        ),
        1 => array (
            'id'             => 1,
            'status'         => 0,
            'price'          => 10000.00,
            'currency'       => 'EUR',
            'exchange_rate'  => 1.00,
            'vat_id'         => 1,
            'vat_percentage' => 21.00,
            'created_by'     => 400,
            'created_at'     => '2012-02-29 10:11:12',
            'updated_by'     => 303,
            'updated_at'     => '2012-11-04 03:01:01',
        ),
    );
}

The next thing we need to come up with is how to mock out the data gateway as we don't want to connect to the database of course. Because my mapper class Order_Model_OrderMapper has 2 methods to set and retrieve the data gateway it's easy to flip in and out the data gateway object. Here's my Order_Model_OrderMapper class:

<php
class Order_Model_OrderMapper
{
    /**
     * @var Zend_Db_Table_Abstract
     */
    protected $_dbTable;
    /**
     * Sets the data gateway for this Mapper class
     * 
     * @param Zend_Db_Table $dbTable
     * @throws In2it_Model_Exception
     */
    public function setDbTable($dbTable)
    {
        if (is_string($dbTable)) {
            if (!class_exists($dbTable)) {
                throw new In2it_Model_Exception(
                    'Non-existing data gateway provided'
                );
            }
            $dbTable = new $dbTable;
        }
        if (!$dbTable instanceof Zend_Db_Table_Abstract) {
            throw new In2it_Model_Exception('Invalid data gateway provided');
        }
        $this->_dbTable = $dbTable;
    }
    /**
     * Retrieves the data gateway class from this Mapper class
     * 
     * @return Zend_Db_Table
     * @throws In2it_Model_Exception 
     */
    public function getDbTable()
    {
        if (!isset ($this->_dbTable)) {
            throw new In2it_Model_Exception('Data gateway not set');
        }
        return $this->_dbTable;
    }

    ...
}

All we need to do now is create a mock object that we can inject in our mapper test so we don't need to connect to the database in order to test our mapper logic.

public function testOrderMapperCanFindSingleItem()
{
    $order = new Order_Model_Order();
    $orderMapper = new Order_Model_OrderMapper();
        
    $mockDb = $this->getMock('Order_Model_DbTable_Order', array ('find'));
    $mockDb->expects($this->atLeastOnce())
           ->method('find')
           ->will($this->returnValue(new ArrayIterator($this->_order)));
    $orderMapper->setDbTable($mockDb);

    $orderMapper->find($order, 1);
    $this->assertEquals($this->_order[0], $order->toArray());
}

So we create our $mockDb object replacing our data gateway object Order_Model_DbTable_Order, and by overriding the find method we can now control how we want this method to operate. By returning an ArrayIterator object, we can use the iterator method current() which is used by the find method in our mapper class. All that's left is to find our order and assert it's the same as our fixture.

I hope this might give a few ideas on saving time running your tests. The less you have expensive connections, the quicker your tests will execute.
Creative Commons License
This work is licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 License.