It's not PHPUnit, PHP, Legacy Code or Obscure APIs ... it's you!
Introduction … Stop Making Excuses … The actual reason you don’t write tests …
It seems weird doesn’t it? You barely find any Ruby on Rails codebase not containing at least an attempt at a test suit, while a large number of PHP projects seem to run PHPUnit like vampires from sunlight.
So, how did this come about? Maybe unit-testing PHP is harder than re-writing WordPress in pure C? Not really … people just don’t know what they’re doing, that’s all … you’ll see. But … let’s look at the psychology behind this first.
The Nice Explanation
Ok, let’s start this out the nice way. PHP simply came before say Ruby on Rails in terms of being popular. For the sake of argument say … PHP was hyped a lot around 2000-2002. Back in those days, PHPUnit wasn’t even around. According to Wikipedia it had it’s initial release in March 2004.
So a reasonable argument could simply go along the lines of many PHP developers not having been introduced to unit-testing when they started out and the lack of adoption of TDD and the like in PHP projects is simply a collective bad habit I guess.
The not so nice way of putting it …
Honestly … I don’t buy the above. The problem is much different in my opinion, at least with some people. PHP simply is an incredibly easy language. It neither suggests nor enforces many good practices. When setting up your Rails project, Rails at least gives you a folder structure suggesting proper separation of your models, views, controllers and whatnot. PHP and many popular frameworks built on it, like say WordPress do not do that. You’re free to do whatever the hell you want! No clue on programming beyond trivial control flow … No Problem!
Just write some HTML, and whenever you feel like adding some dynamic behavior throw in the good old <?php //crappy code ?>
every now and then and feel like a developer.
This will work out surprisingly well up to a certain project size. Sooner or later though you’ll find yourself going back and forth between random bug A rendering some box in one corner of the screen wrong and random bug B throwing a notice every now and then.
Your project simply will hit that point, no matter how careful you are. Some people can delay the onset of this longer by being paranoid and conscientious … others with bad concentration will hit it sooner.
Eventually the all have to face one fairly obvious fact, their skill-set simply does not allow for a project beyond a certain size X. This is not PHP’s fault, it’s the programmers fault, I’m sorry ( not really ) … it’s true.
You can test it all … just watch and learn …
So, lets look at some of the general issues people are having with this and see how you can work around them in the real world.
Example One: Crappy Global Function in the API
So lets assume you’re working with “WeirdoCMS”. WeirdoCMS has a fun little public function in it’s API that does some crazy magic, eventually accessing 21 different tables just to return you the number of FOOs …whatever those may be.
Let’s call that function FooCount
.
Now you got yourself the task of fixing this thing in your WeirdoPlugin:
Damn … someone wasn’t even able to find the 2 on his keyboard it seems :/ Wouldn’t it be great if we could turn that 3 into the correct 2, while also preventing this from ever happening again with a test? It sure would be …
How to do it wrong
So people generally do this when faced with the above illustrated issue of having a trivial thing to test, but no idea of how to test it on account of the API interfacing with their code being all but trivial (remember 21 tables). The more motivated ones within the incompetent population might actually try to tackle this by actually setting up those 21 tables correctly only to run their example like so:
After actually having gone through this pain once, they’ll probably also not do it again and simply state that their project does not have the time and budget for unit-testing. Most will probably not even get this far, they’ll simply give up without shelling out those hundreds of lines of weird and crappy test code.
How to get it right
Step 1: Drop the Procedural Programming, we’ve entered the OOP era quite some time ago …
So let’s encapsulate our function in an object first, many will try this step anyways … just because everyone seems to be doing it. And let’s also try being a little full of ourselves and “apply correct OOP principles” while talking out of our behinds mostly … to put it mildly.
So instead of evenNumberOfFoo
we now use:
That didn’t really accomplish much except for turning
into the more verbose
Apart from making the code longer we accomplished exactly nothing so far I fear … also the code is now a little slower with all that object instantiation overhead. That can’t be it with those OOP principles … this seems to make things even worse than before. We still can’t test this and now it’s longer and slower …
Step 2: Learn the word “Mocking” …
Now after some googling around you finally figure it out, you’re not the first person to run into the above issue. As a matter of fact, it seems half the PHP dev population has too.
So you learn that using that magic Mocking thing, you can actually force FooCount()
to return whatever you want it to apparently. But … again life is fll of disappointments :/ … seems you can only do this with actual class methods.
Your API is given to you in the form of a men old global function though. You might start torturing Google a little more, but eventually you’ll find out … there is absolutely no way of mocking those global functions.
No unit-testing using WeirdoCMS it seems, it has it’s API presented to you in the form of global functions and there is just no way of mocking it and since that’s so unit-testing is just way too hard, impossible or time-consuming. You can even tell your boss that it’s not your fault, all those bugs suck … but hey WeirdoCMS apparently does not allow you to unit-test, so on to the next 40 regressions in your bug-tracker …
Sounds like you :)? Check out the next step, you’ll see … it’s gonna be all better soon :)
Step 3: Learn how to “mock”
Well, it doesn’t take a genius to figure out that if you can only mock and hence fake the return of your number of Foos when it’s given to you by a non-static class method, then you simply have to return it by a non-static class method. In come wrapping and dependency injection :)
So let’s wrap that function call in the form of a method call, lets do it like this:
So with this we could turn out Class into this I suppose:
Still you simply can’t seem to figure out how to mock the FooCount … after all it seems you can only mock the return of the method on specific instances of the MyWeirdCMSApiWrapper and not on all instances. But as you’re only instantiating those object when calling the method you’re trying to test, your test setup simply cannot force them to return anything specific.
In comes dependency injection to the rescue. Something had to come to the rescue right? I mean … if you think about it, your new MyWeirdCMSApiWrapper()
essentially is just a different public method being called. You just moved the issue, you did not fix it!
So, what is this dependency injection thing? Well, it just means that you do not instantiate objects inside of your methods (with exceptions obviously … otherwise you would never get any instances of anything I guess :)) or use global variables for the objects you’re trying mock, but just inject them via the constructor of your to be tested classes.
So instead of creating an instance of your API wrapper, you just pass it to the constructor of your class like so:
Now instead of a crappy test, we can actually make a nice test, by mocking that $apiWrapper
we’re passing in.
Enjoy the beauty of proper OOP design and testing :)
BAM! Sure the above suffers from duplicate code, but for now … this is a much nicer test than expected isn’t it and trivial to create too!
Working from this you could obviously start substituting your globals for injected dependencies, your singletons for class you can actually instantiate freely and wrap APIs nicely and well … you might actually stop bitching and start testing :)
To be continued …
Example Two soon … though seriously … the above should give you all the material you need to circumvent all those crappy and tricky testing spots shouldn’t it ? :)