A few times we already had gone over unit testing… and why it is a good thing to do.
If you had been interested in this topic for a while, then I’m sure you’ve come across the concept of “Mock Objects”.
But really, what are they? And more importantly how do we use them?
So, to get started let’s look at the “official” mock object definition.
http://en.wikipedia.org/wiki/Mock_object
In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways. A programmer typically creates a mock object to test the behavior of some other object, in much the same way that a car designer uses a crash test dummy to simulate the dynamic behavior of a human in vehicle impacts.
Actually, this is a pretty good analogy. It does make sense and lets us work out a real example.
In CakePHP there are several examples to work with “mocked” objects, especially when emulating some core things, like Session, Email, etc.
This is already nicely covered in the manual.
So what about some real world scenario?
I’d say the first thing that happens very often in the real world, is that you have to retrofit your unit tests to “prove” that an already existing production code is working.
In other words, you don’t write the tests up-front, but you need to write tests for an existing production/application code.
Let’s look at an example… we can imagine a random model with a method like this one:
$provider = $this->getProvider($id);
if ($provider) {
$info = $provider->getInfo();
if ($info == '200') {
return 'got 200';
} else {
return 'did not get 200';
}
}
return false;
}
If we are about to write a unit test for this method, then the first thing we have to figure out is… just what is $provider.. ?
Looking at the code it seems to drive all the little logic in this method.
Once we have a provider, we’d call its method getInfo() to see what sort of response we get back.
This seems simple enough, but what if our $povider needs a bunch of keys and access to a certain IP-restricted area and even to getInfo(), it costs $15 per query? :(
We can’t test or utilize the real $provider Class/Object… so this is a good time to just “mock” one up.
If we look at the code that needs to be tested, we don’t really care how $provider works internally. (Yes, it should be tested also.. but this test has nothing to do with internal workings of the $provider).
Anyway, we do care that if we call the getInfo() method it should return ‘200’. This is the important logic of our application, which we are trying to test.
Sounds good, so if we can fake a return value of ‘200’, we can presume that the provider is doing what it is supposed to do and our unit test for the checkIt() method is done.
Let’s take a look at our unit test:
$provider = $this->getMock('OurProvider', array('getInfo'));
$provider->expects($this->any())
->method('getInfo')
->will($this->returnValue('200'));
$result = $this->Account->checkIt(1);
$this->assertEquals('got 200', $result);
}
We just mocked up a simple $provider object. We gave it one method: getInfo().
$provider->expects($this->any())->method(‘getInfo’), basically tells our test that we are going to run this method. (If we look back to the model code: $info = $provider->getInfo(), then yeah this is exactly what is supposed to happen).
Of course we also expect a value of ‘got 200’ to be returned by our method.
In our mocked object, we ensure this by doing ->will($this->returnValue(‘200’));. As far as we are concerned as long as ‘200’ is returned and the if ($info == ‘200’) of our model’s method is evaluated correctly, then we are happy.
We expect the return ‘got 200’; to fire correctly in our unit test $this->assertEquals(‘got 200’, $result);. If we are going to run this test right now, it would unfortunately (fortunately) fail because the $provider object used by the model’s code is relying on the actual one, which probably has no proper way of running the getInfo() method.
So far, so good… but there is one obvious problem with our code and the test. The current model code uses the $provider that’s part of some model logic… and not at all the little object that we had mocked earlier.
Now then, we fall back on the technique, which I think is called Dependency Injection. This is exactly where some people have a problem with a tight coupling between unit tests and business logic. Albeit, harmless, there is still a few “extra” lines of code that we have to add to our model to properly test it.
Basically we have to “inject” our mocked $provider object.
if (is_null($provider)) {
$provider = $this->getProvider($id);
}
if ($provider) {
$info = $provider->getInfo();
if ($info == '200') {
return 'got 200';
} else {
return 'did not get 200';
}
}
return false;
}
Now we can pass the $provider object to the model’s checkIt() method…
Yeah, we’ll modify our test and make sure that we pass our mocked object to properly test the code:
$provider = $this->getMock('OurProvider', array('getInfo'));
$provider->expects($this->any())
->method('getInfo')
->will($this->returnValue('200'));
$result = $this->Account->checkIt(1, $provider);
$this->assertEquals('got 200', $result);
}
The test should be passing now.
I personally feel that the use of objects in such manners actually helps to properly test, but more importantly refactor the code. If we see that a Dependency Injection is required, for example, we have a few things to consider.
- Do we have good tests for our $provider
- Can we avoid Dependency Injection and possibly refactor the model code?