06 Feb 2014 Unit Testing PHP and Silex using PHPUnit
As a newcomer to PHP I was puzzled by how to unit test controllers and services when using Silex (if you’re wondering, Silex is based on the Symfony 2 framework and draws a lot of modules from it).
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 IDENT
s, 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.
TestCaseBase
I have created a Helper Test class called TestCaseBase
that extends Silex WebTestCase
.
The original 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 test
flag.
PHPUnit
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.
Conclusion
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.
Acknowledgements
Thanks to Mark Moloney of Shine for the introduction to Silex and sharing PHP knowledge.
No Comments