Unit Testing Data Repositories

Unit testing is something that I'm still rough around the edges with. I don't do it nearly enough at work and never really pushed myself to pick it up on the side. With my core site refactor, though, it seemed like a good time to wander into testing. And it made a lot of sense to write unit tests around the new domain repository classes I was building out.

As a quick refresher, a repository is merely an encapsulation of the business logic that defines a collection. Or, you know, a query. The application makes a call out to a method, that method performs a query (or another storage-specific data retrieval operation) based on the domain, and then some sort of collection is returned. In order to test a repository there needs to be a storage area with mocked data to check to make sure the business logic is being respected.

Right now I only have MySql-based repository objects, each of which hold onto a few queries, so I needed a way to mock up a MySql-like storage area. Luckily there is a great transient solution for this: in-memory SQLite. I could create a handful of mock tables in memory, test my repository object, and then flush them out of existence.

  1. // example class and method to be tested

  2. class MysqlExampleRepository implements ExampleRepositoryInterface

  3. {

  4. public function __construct($db)

  5. {

  6. $this->db = $db;

  7. }

  8. public function getRow($id)

  9. {

  10. $query = "

  11. SELECT `id`, `title`

  12. FROM `example`

  13. WHERE `id` = :id";

  14. $bindings = [

  15. 'id' => $id,

  16. ];

  17. return $this->db->fetchRow($query);

  18. }

  19. }

  20. // test class

  21. class MysqlExampleRepositoryTest extends PHPUnit_Framework_TestCase

  22. {

  23. public function __construct()

  24. {

  25. $this->db = new ExtendedPdo('sqlite::memory:');

  26. $this->db->exec("

  27. CREATE TABLE IF NOT EXISTS `example` (

  28. `id` integer PRIMARY KEY AUTOINCREMENT,

  29. `title` varchar(60) NOT NULL

  30. );"

  31. );

  32. }

  33. public function testGetRow()

  34. {

  35. $test_data = [

  36. 'id' => 1,

  37. 'title' => 'Hello World',

  38. ];

  39. $this->db->perform("

  40. INSERT INTO `example`

  41. (`id`, `title`)

  42. VALUES

  43. (:id, :title)",

  44. $test_data

  45. );

  46. $repository = new MysqlExampleRepository($this->db);

  47. $fetched_data = $repository->getRow($test_data['id']);

  48. $this->assertSame($test_data['title'], $fetched_data['title']);

  49. }

  50. public static function tearDownAfterClass()

  51. {

  52. $this->db->disconnect();

  53. }

  54. }

As the repository objects grow larger it may make sense to create data sources to automate the hydration part, but the basic gist is to create a mirror copy of the table in SQLite and then inject the connection into the repository for testing. This only works if the queries are cross-engine friendly, of course. Anything too complex or too fine-tuned for a certain flavor of SQL would need some special attention. This is when something like a query builder could come in handy.

Now, there are two things that I'm struggling with for my implementation. The first is that my repository objects always have a domain defined in the query. In SQLite a new database means a new file, and a named database means a named file means that the in-memory option won't work. To get around this I merely switch databases with the 'ATTACH DATABASE' command and then do an unlink during 'tearDownAfterClass'. Not my favorite, but it works.

The other thing I'm struggling with is the storage engine injection. I'm using Aura.Sql and have found it convenient to pass in an instance of Aura\Sql\ConnectionLocator into my repository objects. But now I need to mock up some PDO instances, pass them to a ConnectionLocator, and then pass that around. I may switch to injecting PDO instances into the repositories, yet even that would feel better if PHP offered a PDOInterface. I'm still on the fence with which way to go moving forward.

Anyways, that's the basics on testing PHP repositories. For more examples you can check out my framework on Github, which is still under active development.