There are two styles of testing: “black box” and “white box” styles. Black box testing focuses on the object’s state; whereas, white box testing focuses on behavior. The two styles complement each other and can be combined to thoroughly test code. Mocking allows us to test behavior, and this tutorial combines the mocking concept with TDD to build an example class that uses several other components to achieve its goal.
Step 1: Introduction to Behavior Testing
Objects are entities that send messages to each other. Each object recognizes a set of messages that it in turn answers to. These are public methods on an object. Private methods are the exact opposite. They are completely internal to an object and cannot communicate with anything outside of the object. If public methods are akin to messages, then private methods are similar to thoughts.
The total of all methods, public and private, accessible through public methods represent the behavior of an object. For example, telling an object to move causes that object to not only interact with its internal methods, but also with other objects. From the user’s point of view, the object has just one simple behavior: it moves.
From the programmer’s point of view, however, the object has to do a lot of little things to achieve the movement.
For example, imagine that our object is a car. In order for it to move, it must have a running engine, be in the first gear (or reverse), and the wheels have to turn. This is a behavior we need to test and build upon in order to design and write our production code.
Step 2: Remote Controlled Toy Car
Our tested class never actually uses these dummy objects.
Let’s imagine we are building a program to remote control a toy car. All commands to our class come through the remote control. We have to create a class that understands what the remote control sends and issues commands to the car.
This will be an exercise application, and we presume that the other classes controlling the various parts of the car are already written. We know the exact signature of all these classes, but unfortunately, the car manufacturer could not send us a prototype–not even the source code. All we know are the names of the classes, the methods they have, and what behavior each method encapsulates. The return values are also specified.
Step 3: Application Schema
Here is the complete schema of the application. There’s no explanation at this point; simply keep it in mind for later reference.
Step 4: Test Doubles
A test Stub is an object to control the indirect input of the tested code.
Mocking is a style of testing that requires its own set of tools, a set of special objects representing different levels of faking the behavior of object. These are:
- dummy objects
- test stubs
- test spies
- test mocks
- test fakes
Each of these objects have their special scope and behavior. In PHPUnit, they are created with the $this->getMock()
method. The difference is how and for what reasons the objects are used.
To better understand these objects, I will implement the “Toy Car Controller” step by step using the types of objects, in order, as listed above. Each object in the list is more complex than the object before it. This leads to an implementation that is radically different than in the real world. Also, being an imaginary application, I will use some scenarios that may not even be feasible in a real toy car. But hey, let’s imagine what we need them in order to understand the bigger picture.
Step 5: Dummy Object
Dummy objects are objects that the System Under Test (SUT) depends on, but they are actually never used. A dummy object can be an argument passed to another object, or it can be returned by a second object and then passed to a third object. The point is, our tested class never actually use these dummy objects. At the same time, the object must resemble a real object; otherwise, the receiver may refuse it.
The best way to exemplify this is to imagine a scenario; the schema of which, is below:
The orange object is the RemoteControlTranslator
. It’s main purpose is to receive signals from the remote control and translate them into messages for our classes. At some point, the user will do a “Ready to Go” action on the remote control. The translator will receive the message and create the classes necessary to make the car ready to go.
The manufacturer said that “Ready to Go” means that the engine is started, the gearbox is in neutral, and the lights are set to on or off as per user request.
This means that the user can predefine the state of the lights before being ready to go, and they will turn on or off based on this predefined value on activation. RemoteControlTranslator
then sends all necessary information to the CarControl
class’ getReadyToGo($engine, $gearbox, $electronics, $lights)
method. I know this is far from a perfect design and violates a few principles and patterns, but it is very good for this example.
Start our project with this initial file structure:
Remember, all the classes in the CarInterface folder are provided by the car’s manufacturer; we don’t know their implementation. All we know are the class signatures, but we don’t care about them at this point.
Our main goal is to implement the CarController
class. In order to test this class, we need to imagine how we want to use it. In other words, we put our self in the shoes of the RemoteControlTranslator
and/or any other future class that may use CarController
. Let’s start by creating the a case for our class.
class CarControllerTest extends PHPUnit_Framework_TestCase { }
Then add a test method.
function testItCanGetReadyTheCar() { }
Now think about what we need to pass to the getReadyToGo()
method: an engine, a gearbox, an electronics controller, and light information. For the sake of this example, we will only mock the lights:
require_once '../CarController.php'; include '../autoloadCarInterfaces.php'; class CarControllerTest extends PHPUnit_Framework_TestCase { function testItCanGetReadyTheCar() { $carController = new CarController(); $engine = new Engine(); $gearbox = new Gearbox(); $electornics = new Electronics(); $dummyLights = $this->getMock('Lights'); $this->assertTrue($carController->getReadyToGo($engine, $gearbox, $electornics, $dummyLights)); } }
This will obviously fail with:
PHP Fatal error: Call to undefined method CarController::getReadyToGo()
Despite the failure, this test gave us a starting point for our CarController
implementation. I included a file, called autoloadCarInterfaces.php, that was not on the initial list. I realized I needed something to load the classes, and I wrote a very basic solution. We can always rewrite it when the real classes are provided, but that is an entirely different story. For now, we’ll stick with the easy solution:
foreach (scandir(dirname(__FILE__) . '/CarInterface') as $filename) { $path = dirname(__FILE__) . '/CarInterface/' . $filename; if (is_file($path)) { require_once $path; } }
I presume this class loader is obvious to everybody; so, let’s discuss the test code.
First, we create an instance of CarController
, the class we want to test. Next, we create instances of all the other classes we care about: engine, gearbox, and electronics.
We then create a dummy Lights
object by calling PHPUnit’s getMock()
method and passing the name of the Lights
class. This returns an instance of Lights
, but every method returns null
–a dummy object. This dummy object cannot do anything, but it gives our code the interface necessary to work with Light
objects.
It is very important to note that
$dummyLights
is aLights
object, and any user expecting aLight
object can use the dummy object without knowing that it isn’t a realLights
object.
To avoid confusion, I recommend specifying a parameter’s type when defining a function. This forces the PHP runtime to type check the arguments passed to a function. Without specifying the data type, you can pass any object to any parameter, which can result in the failure of your code. With this in mind, let’s examine the Electronics
class:
require_once 'Lights.php'; class Electronics { function turnOn(Lights $lights) {} }
Let’s implement a test:
class CarController { function getReadyToGo(Engine $engine, Gearbox $gearbox, Electronics $electronics, Lights $lights) { $engine->start(); $gearbox->shift('N'); $electronics->turnOn($lights); return true; } }
As you can see, the getReadyToGo()
function used the $lights
object for the only purpose of sending it to the $electronics
object’s turnOn()
method. Is this the ideal solution for such a situation? Probably not, but you can clearly observe how a dummy object, with no relation whatsoever to the getReadyToGo()
function, is passed along to the one object that really needs it.
Please note that all the classes contained in the CarInterface directory provide dummy objects when initialized. Also assume that, for this exercise, we expect the manufacturer to provide the real classes in the future. We cannot rely on their current lack of functionality; so, we must ensure that our tests pass.
Step 6: “Stub” the Status and Go Forward
A test Stub is an object to control the indirect input of the tested code. But what is indirect input? It is a source of information that can not be directly specified.
The most common example of a test stub is when an object asks another object for information and then does something with that data.
Spies, by definition, are more capable stubs.
The data can only be obtained by asking a specific object for it, and in many cases, these objects are used for a specific purpose inside the tested class. We do not want to “new up” (new SomeClass()
) a class inside of another class for testing purposes. Therefore, we need to inject an instance of a class that acts like SomeClass
without injecting an actual SomeClass
object.
What we want is a stub class, which then leads to dependency injection. Dependency injection (DI) is a technique that injects an object into another object, forcing it to use the injected object. DI is common in TDD , and it is absolutely required in almost any project. It provides a simple way to force an object to use a test-prepared class instead of a real class used in the production environment.
Let’s make our toy car move forward.
We want to implement a method called moveForward()
. This method first queries a StatusPanel
object for the fuel and engine status. If the car is ready to go, then the method instructs the electronics to accelerate.
To better understand how a stub works, I will first write the code for the status check and acceleration:
function goForward(Electronics $electronics) { $statusPanel = new StatusPanel(); if($statusPanel->engineIsRunning() && $statusPanel->thereIsEnoughFuel()) $electronics->accelerate (); }
This code is pretty simple, but we do not have a real engine or fuel to test our goForward()
implementation. Our code won’t even enter the if
statement because we don’t have a StatusPanel
class. But if we continue with the test, a logical solution starts to emerge:
function testItCanAccelerate() { $carController = new CarController(); $electronics = new Electronics(); $stubStatusPanel = $this->getMock('StatusPanel'); $stubStatusPanel->expects($this->any())->method('thereIsEnoughFuel')->will($this->returnValue(TRUE)); $stubStatusPanel->expects($this->any())->method('engineIsRunning')->will($this->returnValue(TRUE)); $carController->goForward($electronics, $stubStatusPanel); }
Line by line explanation:
I love recursion; it is always easier to test recursion than loops.
- create a new
CarController
- create the dependent
Electronics
object - create a mock for the
StatusPanel
- expect to call
thereIsEnoughFuel()
zero or more times and returntrue
- expect to call
engineIsRunning()
zero or more times and returntrue
- call
goForward()
withElectronics
andStubbedStatusPanel
object
This is the test we want to write, but it will not work with our current implementation of goForward()
. We have to modify it:
function goForward(Electronics $electronics, StatusPanel $statusPanel = null) { $statusPanel = $statusPanel ? : new StatusPanel(); if($statusPanel->engineIsRunning() && $statusPanel->thereIsEnoughFuel()) $electronics->accelerate (); }
Our modification uses dependency injection by adding a second optional parameter of type StatusPanel
. We determine if this parameter has a value and create a new StatusPanel
if $statusPanel
is null. This ensures that a new StatusPanel
object is created in production while still allowing us to test the method.
It is important to specify the type of the $statusPanel
parameter. This ensures that only a StatusPanel
object (or an object of an inherited class) can be passed to the method. But even with this modification, our test is still not complete.
Step 7: Complete the Test with a Real Test Mock
We have to test mock an Electronics
object to ensure our method from Step 6 calls accelerate()
. We can’t use the real Electronics
class for several reasons:
- We don’t have the class.
- We cannot verify its behavior.
- Even if we could call it, we should test it in isolation.
A test mock is an object that is capable of controlling both indirect input and output, and it has a mechanism for automatic assertion on expectations and results. This definition may sound a little confusing, but it is really quite simple to implement:
function testItCanAccelerate() { $carController = new CarController(); $electronics = $this->getMock('Electronics'); $electronics->expects($this->once())->method('accelerate'); $stubStatusPanel = $this->getMock('StatusPanel'); $stubStatusPanel->expects($this->any())->method('thereIsEnoughFuel')->will($this->returnValue(TRUE)); $stubStatusPanel->expects($this->any())->method('engineIsRunning')->will($this->returnValue(TRUE)); $carController->goForward($electronics, $stubStatusPanel); }
We simply changed the $electronics
variable. Instead of creating a real Electronics
object, we simply mock one.
On the next line, we define an expectation on the $electronics
object. More precisely, we expect that the accelerate()
method is called only one time ($this->once()
). The test now passes!
Feel free to play around with this test. Try changing $this->once()
into $this->exactly(2)
and see what a nice failure message PHPUnit gives to you:
1) CarControllerTest::testItCanAccelerate Expectation failed for method name is equal to <string:accelerate>; when invoked 2 time(s). Method was expected to be called 2 times, actually called 1 times.
Step 8: Use a Test Spy
A test spy is an object capable of capturing indirect output and providing indirect input as needed.
Indirect output is something we can't directly observe. For example: when the tested class computes a value and then uses it as an argument for another object's method. The only way to observe this output is to ask the called object about the variable used to access its method.
This definition makes a spy almost a mock.
The main difference between a mock and spy is that mock objects have built-in assertions and expectations.
In that case, how can we create a test spy using PHPUnit's getMock()
? We can't (well, we can't create a pure spy), but we can create mocks capable of spying other objects.
Let's implement the braking system so we can stop the car. Braking is really simple; the remote control will sense the braking intensity from the user and send it to the controller. The remote also provides an "Emergency Stop!" button. This must instantly engage brakes with maximum power.
The braking power measures values ranging from 0 to 100, with 0 meaning nothing and 100 meaning maximum brake power. The "Emergency Stop!" command will be received as different call.
The CarController
will issue a message to the Electronics
object to activate the brake system. The car controller can also query the StatusPanel
for speed information obtained through sensors on the car.
Implementation Using a Pure Test Spy
Let's first implement a pure spy object without using PHPUnit's mocking infrastructure. This will give you a better understanding of the test spy concept. We start by checking the Electronics
object's signature.
class Electronics { function turnOn(Lights $lights) {} function accelerate(){} function pushBrakes($brakingPower){} }
We're interested in the pushBrakes()
method. I didn't call it brake()
to avoid confusion with the break
keyword in PHP.
To create a real spy, we will extend Electronics
and override the pushBrakes()
method. This overridden method will not push the brake; instead, it will only register the braking power.
class SpyingElectronics extends Electronics { private $brakingPower; function pushBrakes($brakingPower) { $this->brakingPower = $brakingPower; } function getBrakingPower() { return $this->brakingPower; } }
The the getBrakingPower()
method gives us the ability to check the braking power in our test. This is not a method we would use in production.
We can now write a test capable of testing the braking power. Following TDD principles, we'll start with the simplest test and provide the most basic implementation:
function testItCanStop() { $halfBrakingPower = 50; $electronicsSpy = new SpyingElectronics(); $carController = new CarController(); $carController->pushBrakes($halfBrakingPower, $electronicsSpy); $this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower()); }
This test fails because we do not yet have a pushBrakes()
method on CarController
. Let's rectify that and write one:
function pushBrakes($brakingPower, Electronics $electronics) { $electronics->pushBrakes($brakingPower); }
The test now passes, effectively testing the pushBrakes()
method.
We can also spy on method calls. Testing the StatusPanel
class is the next logical step. It provides the user different pieces of information regarding the remote controlled car. Let's write a test that checks if the StatusPanel
object is asked about the car's speed. We'll create a spy for it:
class SpyingStatusPanel extends StatusPanel { private $speedWasRequested = false; function getSpeed() { $this->speedWasRequested = true; } function speedWasRequested() { return $this->speedWasRequested; } }
Then, we modify our test to use the spy:
function testItCanStop() { $halfBrakingPower = 50; $electronicsSpy = new SpyingElectronics(); $statusPanelSpy = new SpyingStatusPanel(); $carController = new CarController(); $carController->pushBrakes($halfBrakingPower, $electronicsSpy, $statusPanelSpy); $this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower()); $this->assertTrue($statusPanelSpy->speedWasRequested()); }
Note that I did not write a separate test.
The recommendation of "one assertion per test" is good to follow, but when your test describes an action requiring several steps or states, using more than one assertion in the same test is acceptable.
Even more, this keeps your assertions about a single concept in one place. This helps eliminate duplicate code by not requiring you to repeatedly set up the same conditions for your SUT.
And now the implementation:
function pushBrakes($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) { $statusPanel = $statusPanel ? : new StatusPanel(); $electronics->pushBrakes($brakingPower); $statusPanel->getSpeed(); }
There's just a small, tiny thing bothering me: the name of this test is testItCanStop()
. That clearly implies that we push the brakes until the car comes to a complete stop. We, however, called the method pushBrakes()
, which is not quite correct. Time to refactor:
function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) { $statusPanel = $statusPanel ? : new StatusPanel(); $electronics->pushBrakes($brakingPower); $statusPanel->getSpeed(); }
Don't forgot to change the method call in the test also.
$carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy);
Indirect output is something we can't directly observe.
At this point, we need to think about our braking system and how it works. There are several possibilities, but for this example, assume that the provider of the toy car specified that braking happens in discreet intervals. Calling an Electronics
object's pushBreakes()
method pushes the brake for a discreet amount of time and then releases it. The time interval is unimportant to us, but let's imagine it is a fraction of a second. With such a small time interval, we have to continuously send pushBrakes()
commands until the speed is zero.
Spies, by definition, are more capable stubs, and they can also control indirect input if needed. Let's make our StatusPanel
spy more capable and offer some value for the speed. I think the first call should provide a positive speed--let's say the value of 1
. The second call will provide the speed of 0
.
class SpyingStatusPanel extends StatusPanel { private $speedWasRequested = false; private $currentSpeed = 1; function getSpeed() { if ($this->speedWasRequested) $this->currentSpeed = 0; $this->speedWasRequested = true; return $this->currentSpeed; } function speedWasRequested() { return $this->speedWasRequested; } function spyOnSpeed() { return $this->currentSpeed; } }
The overridden getSpeed()
method returns the appropriate speed value via the spyOnSpeed()
method. Let's add a third assertion to our test:
function testItCanStop() { $halfBrakingPower = 50; $electronicsSpy = new SpyingElectronics(); $statusPanelSpy = new SpyingStatusPanel(); $carController = new CarController(); $carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy); $this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower()); $this->assertTrue($statusPanelSpy->speedWasRequested()); $this->assertEquals(0, $statusPanelSpy->spyOnSpeed()); }
According to the last assertion, the speed should have a speed value of 0
after the stop()
method finishes execution. Running this test against our production code results in a failure with a cryptic message:
1) CarControllerTest::testItCanStop Failed asserting that 1 matches expected 0.
Let's add our own custom assertion message:
$this->assertEquals(0, $statusPanelSpy->spyOnSpeed(), 'Expected speed to be 0 (zero) after stopping but it actually was ' . $statusPanelSpy->spyOnSpeed());
That produces a much more readable failure message:
1) CarControllerTest::testItCanStop Expected speed to be 0 (zero) after stopping but it actually was 1 Failed asserting that 1 matches expected 0.
Enough failures! Let's make it pass.
function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) { $statusPanel = $statusPanel ? : new StatusPanel(); $electronics->pushBrakes($brakingPower); if ($statusPanel->getSpeed()) $this->stop($brakingPower, $electronics, $statusPanel); }
I love recursion; it is always easier to test recursion than loops. Easier testing means simpler code, which in turn means a better algorithm. Check out the The Transformation Priority Premise for more on this subject.
Getting Back to PHPUnit's Mocking Framework
Enough with the extra classes. Let's rewrite this using PHPUnit's mocking framework and eliminate those pure spies. Why?
Because PHPUnit offers better and simpler mocking syntax, less code, and some nice predefined methods.
I usually create pure spies and stubs only when mocking them with getMock()
would be too complicated. If your classes are so complex that getMock()
can't handle them, then you have a problem with your production code--not with you tests.
function testItCanStop() { $halfBrakingPower = 50; $electronicsSpy = $this->getMock('Electronics'); $electronicsSpy->expects($this->exactly(2))->method('pushBrakes')->with($halfBrakingPower); $statusPanelSpy = $this->getMock('StatusPanel'); $statusPanelSpy->expects($this->at(0))->method('getSpeed')->will($this->returnValue(1)); $statusPanelSpy->expects($this->at(1))->method('getSpeed')->will($this->returnValue(0)); $carController = new CarController(); $carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy); }
The total of all methods, public and private, accessible through public methods represent the behavior of an object.
A line by line explanation of the above code:
- set half braking power = 50
- create an
Electronics
mock - expect method
pushBrakes()
to execute exactly two times with the above specified braking power - create a
StatusPanel
mock - return
1
on firstgetSpeed()
call - return
0
on secondgetSpeed()
execution - call the tested
stop()
method on a realCarController
object
Probably the most interesting thing in this code is the $this->at($someValue)
method. PHPUnit counts the amount of calls to that mock. Counting happens on the mock level; so, calling multiple methods on $statusPanelSpy
would increment the counter. This may seem a little counter-intuitive at first; so let's look at an example.
Presume we want to check the fuel level on each call to stop()
. The code would look like this:
function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) { $statusPanel = $statusPanel ? : new StatusPanel(); $electronics->pushBrakes($brakingPower); $statusPanel->thereIsEnoughFuel(); if ($statusPanel->getSpeed()) $this->stop($brakingPower, $electronics, $statusPanel); }
This will break our test. You may be confused why, but you will get the following message:
1) CarControllerTest::testItCanStop Expectation failed for method name is equal to <string:pushBrakes> when invoked 2 time(s). Method was expected to be called 2 times, actually called 1 times.
It is pretty obvious that pushBrakes()
should be called two times. Why then do we get this message? Because of the $this->at($someValue)
expectation. The counter increments as follows:
- first call to
stop()
-> first call tothereIsEnougFuel()
=> internal counter at0
- first call to
stop()
-> first call togetSpeed()
=> internal counter at 1 and return0
- second call to
stop()
never happens => second call togetSpeed()
never happens
Each call to any mocked method on $statusPanelSpy
increments PHPUnit's internal counter.
Step 9: A Test Fake
If public methods are akin to messages, then private methods are similar to thoughts.
A test fake is a simpler implementation of a production code object. This is a very similar definition to test stubs. In reality, Fakes and Stubs are very similar as per external behavior. Both are objects mimicking the behavior of some other real objects, and both implement a method to control indirect input. The difference is that Fakes are much more closer to a real object than to a dummy object.
A Stub is basically a dummy object whose methods return predefined values. A Fake, however, does a complete implementation of a real object in a much simpler way. Probably the most common example is an InMemoryDatabase
to perfectly simulate a real database class without actually writing to the data store. Thus, testing becomes faster.
Test Fakes should not implement any methods to directly control input or return observable state. They are not used to be questioned; they are used to provide--not observe. Most common use cases of Fakes are when the real Dependent on Component (DOC) is not yet written, it is too slow (like a database), or the real DOC is not available in the testing environment.
Step 10: Conclusions
The most important mock functionality is controlling the DOC. It also provides a great way to control indirect I/O with the help of dependency injection techniques.
There are two main opinions about mocking:
Some say that mocking is bad...
- Some say that mocking is bad, and they are right. Mocking does something subtle and ugly: it binds too much of the tests to the implementation. Whenever possible, the test should be as independent on the implementation as possible. Black box testing is always preferable to white box testing. Always test state if you can; don't mock behavior. Being anti-mockist encourages bottom-up development and design. This means that the small component parts of the system are created first and then combined into a harmonious structure.
- Some say that mocking is good, and they are right. Mocking does something subtle and beautiful; it defines behavior. It makes us think much more from the point of view of a user. Mockists usually use a top-down approach to implementation and design. They start with the topmost class in the system and write the first test by mocking some other imaginary DOC that is not yet implemented. The components of the system appear and evolve based on the mocks created at one level higher.
This being said, it's up to you to decide which way to go.
Some prefer to be mockists while others prefer state testing. Each approach has its pros and cons. An all-mocked system offers extra behavioral information in the tests. An all-state system offers more details about the components, but it may also hide some behavior.
Additional References and Books
- The official PHPUnit mocking documentation
- Mocks Aren't Stubs a blog post by Martin Fowler
- The Transformation Priority Premise a blog post by Robert C. Martin
- xUnit Test Patterns: Refactoring Test Code a book by Gerard Meszaros
- Working Effectively with Legacy Code a book by Michael Feather
No hay comentarios:
Publicar un comentario