Skip to main content

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.

Comments

  1. Thanks for sharing what you learned and how you do it, Michelangelo!

    ReplyDelete

Post a Comment

Popular posts from this blog

PHP 7 and Apache on macOS Sierra

I posted several talks about compiling PHP from source, but everyone was trying to convince me that a package manager like Homebrew was a more convenient way to install. The purpose of Homebrew is simple: a package manager for macOS that will allow you to set up and install common packages easily and allows you to update frequently using simple commands. I used a clean installation of macOS Sierra to ensure all steps could be recorded and tested. In most cases you already have done work on your Mac, so chances are you can skip a few steps in this tutorial. APACHE AND PHP WITH HOMEBREW I’ve made this according to the installation instructions given on GetGrav. The installation procedures These installation procedures will set up your macOS Sierra with PHP 7.1 and Apache 2.4. Install Xcode command line tools (if not done yet)xcode-select --install Install Homebrew/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" Set up for in…

Sessions in PHP 7.1 and Redis

In case you have missed it, PHP 7.1.0 has been released recently. Now you can’t wait to upgrade your servers to the latest and greatest PHP version ever. But hold that thought a second… With PHP 7 lots of things have changed underneath the hood. But these changed features can also put unexpected challenges on your path. Our challenge One of these challenges that we faced was getting PHP 7.1 to play nice storing sessions in our Redis storage. In order to store sessions in Redis, we needed to install the Redis PHP extension that not only provides PHP functions for Redis, but also installs the PHP session handler for Redis. Because we upgraded our servers to PHP 7.1, we were looking to use the latest provided version for this Redis extension: redis-3.1.0. Once installed, we bumped against a nasty problem. Warning: session_start(): Failed to read session data: redis (path: tcp://127.0.0.1:6379) Searching the internet for this error, we didn’t got many hits that could point us into a dire…

Speeding up database calls with PDO and iterators

When you review lots of code, you often wonder why things were written the way they were. Especially when making expensive calls to a database, I still see things that could and should be improved.
No framework development When working with a framework, mostly these database calls are optimized for the developer and abstract the complex logic to improve and optimize the retrieval and usage of data. But then developers need to build something without a framework and end up using the basics of PHP in a sub-optimal way.

$pdo = new \PDO( $config['db']['dsn'], $config['db']['username'], $config['db']['password'] ); $sql = 'SELECT * FROM `gen_contact` ORDER BY `contact_modified` DESC'; $stmt = $pdo->prepare($sql); $stmt->execute(); $data = $stmt->fetchAll(\PDO::FETCH_OBJ); echo 'Getting the contacts that changed the last 3 months' . PHP_EOL; foreach ($data as $row) { $dt = new \DateTime('2015-04-…