In the process of learning test-driven development, I’ve been introduced to dependency injection and the use of interfaces, and have started using these concepts in my own PHP code in order to make it more testable.
There have been times when I’ve needed to test code that was doing things like calling the PHP time()
function. In order to make these tests predictable, it seemed logical to create an interface to the standard PHP functions I use so that I can mock them out in my tests.
Is this good software design? What are the pros and cons of doing this? I’ve found myself groaning at how quickly my PHP interface can stick its fingers into everything I do. Is there a better way to make code that relies on PHP-accessed state and functions more testable?
3
You should abstract away calls to time()
like you should abstract away calls to rand()
and its ilk. The reason for doing that is because functions like that are forms of global, non-deterministic state. Global, non-deterministic state makes testing much more difficult because you cannot isolate your tests and test all conditions at once. Imagine if I had to wait until after noon to see if my code said “Good afternoon” instead of “Good morning” or “Good evening”!
If you see your interface starting to permeate your application, this may not be a bad thing. If anything, it is telling you how much you are using these built-in functions. You can depend on you to maintain your code and your interfaces within your code base, so you should not have as many worries about a shifting interface causing shotgun surgery. Abstracting away time()
should be a pretty stable interface and probably should not see any changes for the most part.
The alternative to an interface is to simply inject the result of a call to time()
rather than calling it in the method. So write getGreeting(time());
rather than writing getGreeting($timeUtil);
(which in the method does $time = $timeUtil->getTime()
). This adheres more closely to the Law of Demeter as you are not reaching for one thing through another, but it also means that the time you want must be the time at which the method is invoked.
An alternative alternative is to pass in a function reference rather than generating a separate interface. In PHP, you could do something like:
function getTime($timeFunc) {
return call_user_func($timeFunc);
}
echo getTime('time')."n"; # Call the built-in time function
echo getTime(function() { # Call a custom function
return 1;
})."n";
This has less overhead than the full-blown interface while still allowing the function to be lazily called and allowing dependency injection.
3
You can either
- create interfaces to system state methods like date or the content of session/cookie as you suggested or,
- put these infos into business-method as additional parameters.
Example:
- the invoice module can get the invoice date itself through an interface or,
- the caller of the invoice module can provide the invoice date.
I prefer the 2nd method although this means that every time the module needs more state infos you have to change the method signature. On the other side in testing it is much easier to provide parameter than to mock methods.
I’ve found myself groaning at how quickly
my PHP interface can stick its fingers into everything I do.
If your module needs too many parameters or too many interfaces, this might be an indicator that your module is doing too much and violates “separations of concerns.”
1