Why is testing singletons or registry pattern hard in a language like PHP which is request driven?
You can write and run tests aside from the actual program execution, so that you are free to affect the global state of the program and run some tear downs and initialization per each test function to get it to the same state for each test.
Am I missing something?
While it's true that "you can write and run tests aside of the actual program execution so that you are free to affect the global state of the program and run some tear downs and initialization per each test function to get it to the same state for each test.", it is tedious to do so. You want to test the TestSubject in isolation and not spend time recreating a working environment.
Example
To get this working you have to have the concrete
Registry
. It's hardcoded, and it's a Singleton. The latter means to prevent any side-effects from a previous test. It has to be reset for each test you will run on MyTestSubject. You could add aRegistry::reset()
method and call that insetup()
, but adding a method just for being able to test seems ugly. Let's assume you need this method anyway, so you end up withNow you still don't have the 'MyActiveRecord' object it is supposed to return in
foo
. Because you like Registry, your MyActiveRecord actually looks like thisThere is another call to Registry in the constructor of MyActiveRecord. You test has to make sure it contains something, otherwise the test will fail. Of course, our database class is a Singleton as well and needs to be reset between tests. Doh!
So with those finally set up, you can do your test
and realize you have yet another dependency: your physical test database wasn't setup yet. While you were setting up the test database and filled it with data, your boss came along and told you that you are going SOA now and all these database calls have to be replaced with Web service calls.
There is a new class
MyWebService
for that, and you have to make MyActiveRecord use that instead. Great, just what you needed. Now you have to change all the tests that use the database. Dammit, you think. All that crap just to make sure thatdoSomethingWithResults
works as expected?MyTestSubject
doesn't really care where the data comes from.Introducing mocks
The good news is, you can indeed replace all the dependencies by stubbing or mock them. A test double will pretend to be the real thing.
This will create a double for a Web service that expects to be called once during the test with the first argument to method
findById
being 1. It will return predefined data.After you put that in a method in your TestCase, your
setup
becomesGreat. You no longer have to bother about setting up a real environment now. Well, except for the Registry. How about mocking that too. But how to do that. It's hardcoded so there is no way to replace at test runtime. Crap!
But wait a second, didn't we just say MyTestClass doesn't care where the data comes from? Yes, it just cares that it can call the
findById
method. You hopefully think now: why is the Registry in there at all? And right you are. Let's change the whole thing toByebye Registry. We are now injecting the dependency MyWebSe… err… Finder?! Yeah. We just care about the method
findById
, so we are using an interface nowDon't forget to change the mock accordingly
and setup() becomes
Voila! Nice and easy and. We can concentrate on testing MyTestClass now.
While you were doing that, your boss called again and said he wants you to switch back to a database because SOA is really just a buzzword used by overpriced consultants to make you feel enterprisey. This time you don't worry though, because you don't have to change your tests again. They no longer depend on the environment.
Of course, you still you have to make sure that both MyWebservice and MyActiveRecord implement the Finder interface for your actual code, but since we assumed them to already have these methods, it's just a matter of slapping
implements Finder
on the class.And that's it. Hope that helped.
Additional Resources:
You can find additional information about other drawbacks when testing Singletons and dealing with global state in
This should be of most interest, because it is by the author of PHPUnit and explains the difficulties with actual examples in PHPUnit.
Also of interest are:
Singletons (in all OOP languages, not just PHP) make a particular kind of debugging called unit testing difficult for the same reason that global variables do. They introduce global state into a program, meaning that you can't test any modules of your software that depend on the singleton in isolation. Unit testing should include only the code under test (and its superclasses).
Singletons are essentially global state, and while having global state can make sense in certain circumstances, it should be avoided unless it's necessary.
When finishing a PHP test, you can flush singleton instance like this: