06 Feb 2014 Unit Testing PHP and Silex using PHPUnit
In this post I’ll talk about my experiences getting tests going, and how you can setup your own unit tests using PHPUnit and WebTestCase. I’ll start with my initial attempts to test directly against the DB, and the approach that I ultimately found most useful and practical.
I’ve also created an example Silex project that can be used to try out the various features of PHPUnit. The only prerequisite is PHP 5.4, which comes with most modern OS’s. You don’t even need Apache installed to try it out!
The Problem with Auntie PHP
PHP is basically a scripting language which has evolved over the years and had useful and/or trendy features bolted on. This has made it very powerful and useful in some contexts, but dangerous and frustrating in others. As a development language and ecosystem it has many sharp edges and you will get cut. For more information I recommend the mostly-constructive whining here and here.
Our client historically had been using PHP and, with them being slightly apprehensive about trying out newer tech (such as node.js/MongoDB), we ended up going down the PHP road. Nevertheless, we needed some kind of unit-testing capability.
The other challenge was that we were using SQL Server as the DB, also for historical reasons. That said, I guess it could be worse – we could be coding in XSLT 😉
A First Attempt At Unit Testing
In my first attempt to setup unit tests, I tried to use a dedicated SQL Server DB just for unit testing. It was based on our future production schema.
The first problem was how to ensure data and
IDENT columns were reset so that test results remained consistent between runs.
‘Easy!’, I thought, ‘I can set it up so that tests run in a transaction; for each test I set the data and
IDENTs, plug in the relevant setup data and then everything is rolled back so the DB is restored to the prior state.’
Perhaps with a simpler schema and smaller data requirements this might have worked, but it proved to be too slow. Furthermore, as we could not fire up a private SQL Server instance locally on our Mac/*nix machines, there would be contention to run the tests from a developer machine and the CI server.
Nevertheless, I’ve put a stripped down carcass of that attempt named
DBSQLServerTestCaseBase in the git repo so you can see how to reset
IDENT and clear tables if you ever need to.
A Second (and more traditional) Attempt At Unit Testing
As a second approach, I mocked out relevant service calls so tests for routing, controllers and parts of the business logic could stub any tricky DB/ORM related functionality.
To demonstrate this I’ve created an example app that you can try yourself. It demonstrates how everything hangs together without all the extra guff a real production example would contain.
The Example App
As Silex uses the Pimple dependency-injection framework to setup Controllers and Services, when running Unit Tests you can substitute your Mock Controller or Service as required. Here’s an example of it in action (see
ExampleControllerTest.php for full code):
... $this->app['example.repository'] = $exampleServiceMock; ...
If you look at the sample project, you’ll see that
bootstrap.php is loaded from
index.php. This in-turn loads the Silex framework, loads and registers Controllers and Services, and sets up routes and objects to be injected. You can see the app in action by starting it in a web container and poking it yourself with postman.
I have created a Helper Test class called
TestCaseBase that extends Silex
WebTestCase allows you to create the PHP application in a test context and poke your Controllers from a web-client request context.
TestCaseBase extends this to load your application from
index.php along with your tests when you set a
PHPUnit requires a
phpunit.xml.dist file that points to the test directory location.
The test classes themselves require
test_bootstrap.php to add the ‘Base’ dir to the Silex auto loader.
A Note On Test Doubles
The PHPUnit documentation describes the features better than I can. However there is one important caveat: final, private and static methods cannot be stubbed or mocked and thus always retain their original functionality.
For more information, the PHPUnit documentation is excellent and discusses Test Doubles in detail.
Taking Things Further With AOP
While Pimple provides basic dependency injection, I have recently noticed a PECL extension for AOP. AOP would allow you to address cross-cutting concerns on join points in your PHP code, would add some interesting capabilities to your production code and may even make creating unit tests easier and more powerful.
The options for Unit Testing in PHP are not as powerful or as well-known as those in the ecosystems of platforms like Java or .NET. Nevertheless, having some kind of unit testing in place is important to allow you refactoring confidently, and is also pretty much a precondition if you are to have any sort of continuous integration tool in place for your project. Hopefully this blog will help you move your PHP projects towards this goal.
Thanks to Mark Moloney of Shine for the introduction to Silex and sharing PHP knowledge.