Thursday, December 10, 2009

Who else wants to have free documentation? A readable test code sample

It's fun to TDD classes for your projects because you try your class as its client will do even before both are written at all: interfaces and abstractions are by far the most important part of an application's design. But often test classes grow, and we should ruthlessly refactor them as we will do with production code. One of the most important factors to consider in test refactoring is preserving or improving readability: unit tests are documentation for the smallest components of an application, its classes. New developers that come in contact with production classes take unit tests as the reference point for understanding what a class is supposed to do and which operations it supports.
To give an example, I will report here an excerpt of a personal project of mine, NakedPhp. This code sample is a test case that seems to me particularly well written.

The NakedPhp framework has a container for entity classes (for instance User, City, Post classes). This container is saved in the session and it should be ported to the database for permanent storage when the user wants to save his work.
This is the context where the system under test, the NakedPhp\Storage\Doctrine class, has to work: it is one of the first infrastructure adapter I am introducing. In the test, User entities are stored in a container and they should be merged with the database basing on their state, which can be:
  • new (not present in db);
  • detached (present in db but totally disconnected from the Orm due to their previous serialization; no proxies are references from a detached object and these entities are not kept in the identity map);
  • removed (present in db, but should be deleted as the user decided so).
The NakedPhpStorage\Doctrine::save() method takes a EntityCollection instance and processes the contained objects bridging the application and the database with the help of the Doctrine 2 EntityManager.

This test class is also an example about how to test your classes which require a database, such as Repository implementations. I usually create throw-away sqlite databases, but Doctrine 2 can port the schema to nearly every platform. Using a fake database allows you to write unit test that run independently from database daemons and without having to mock an EntityManager, which has a very long interface. Classes that calculate bowling game scores are nice but classes that store your data a whole lot more.
Finally, I warn you that this example is still basic and will be expanded during future development of NakedPhp. What I want to show here is where the Test-Driven Development style leads and an example of elimination of code duplication and clutter in a test suite.
<?php
/**
 * Naked Php is a framework that implements the Naked Objects pattern.
 * @copyright Copyright (C) 2009  Giorgio Sironi
 * @license http://www.gnu.org/licenses/lgpl-2.1.txt
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * @category   NakedPhp
 * @package    NakedPhp_Storage
 */

namespace NakedPhp\Storage;
use Doctrine\ORM\UnitOfWork;
use NakedPhp\Mvc\EntityContainer;
use NakedPhp\Stubs\User;

/**
 * Exercise the Doctrine storage driver, which should reflect to the database
 * the changes in entities kept in an EntityCollection.
 */
class DoctrineTest extends \PHPUnit_Framework_TestCase
{
    private $_storage;

    public function setUp()
    {
        $config = new \Doctrine\ORM\Configuration();
        $config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache);
        $config->setProxyDir('/NOTUSED/Proxies');
        $config->setProxyNamespace('StubsProxies');

        $connectionOptions = array(
            'driver' => 'pdo_sqlite',
            'path' => '/var/www/nakedphp/tests/database.sqlite'
        );

        $this->_em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);
        $this->_regenerateSchema();

        $this->_storage = new Doctrine($this->_em);
    }

    private function _regenerateSchema()
    {
        $tool = new \Doctrine\ORM\Tools\SchemaTool($this->_em);
        $classes = array(
            $this->_em->getClassMetadata('NakedPhp\Stubs\User')
        );
        $tool->dropSchema($classes);
        $tool->createSchema($classes);
    }

    public function testSavesNewEntities()
    {
        $container = $this->_getContainer(array(
            'Picard' => EntityContainer::STATE_NEW
        ));
        $this->_storage->save($container);

        $this->_assertExistsOne('Picard');
    }

    /**
     * @depends testSavesNewEntities
     */
    public function testSavesIdempotently()
    {
        $container = $this->_getContainer(array(
            'Picard' => EntityContainer::STATE_NEW
        ));
        $this->_storage->save($container);

        $this->_simulateNewPage();
        $this->_storage->save($container);

        $this->_assertExistsOne('Picard');
    }

    public function testSavesUpdatedEntities()
    {
        $picard = $this->_getDetachedUser('Picard');
        $picard->setName('Locutus');
        $container = $this->_getContainer();
        $key = $container->add($picard, EntityContainer::STATE_DETACHED);
        $this->_storage->save($container);

        $this->_assertExistsOne('Locutus');
        $this->_assertNotExists('Picard');
    }

    public function testRemovesPreviouslySavedEntities()
    {
        $picard = $this->_getDetachedUser('Picard');
        $container = $this->_getContainer();

        $key = $container->add($picard, EntityContainer::STATE_REMOVED);
        $this->_storage->save($container);

        $this->_assertNotExists('Picard');
        $this->assertFalse($container->contains($picard));
    }

    private function _getNewUser($name)
    {
        $user = new User();
        $user->setName($name);
        return $user;
    }

    private function _getDetachedUser($name)
    {
        $user = $this->_getNewUser($name);
        $this->_em->persist($user);
        $this->_em->flush();
        $this->_em->detach($user);
        return $user;
    }

    private function _getContainer(array $fixture = array())
    {
        $container = new EntityContainer;
        foreach ($fixture as $name => $state) {
            $user = $this->_getNewUser($name);
            $key = $container->add($user);
            $container->setState($key, $state);
        }
        return $container;
    }

    private function _assertExistsOne($name)
    {
        $this->_howMany($name, 1);
    }

    private function _assertNotExists($name)
    {
        $this->_howMany($name, 0);
    }

    private function _howMany($name, $number)
    {
        $q = $this->_em->createQuery("SELECT COUNT(u._id) FROM NakedPhp\Stubs\User u WHERE u._name = '$name'");
        $result = $q->getSingleScalarResult();
        $this->assertEquals($number, $result, "There are $result instances of $name saved instead of $number.");
    }

    private function _simulateNewPage()
    {
        $this->_em->clear(); // detach all entities
    }
}
Do you have other suggestions to further refactor this code?

3 comments:

Anonymous said...

Hi Giorgio, this post was a little over my head. For example, I'm not entirely clear on what an Entity is. Or Unit of Work that was referenced in the code. Perhaps a 'back to the basics' post explaining these things?

Thanks and I love your blog! It's exteremely useful and has exposed me to much that I have been unaware of.

Giorgio said...

I'll write about the basis of ORMs soon.. :)

Anonymous said...

Thanks!

ShareThis