At some point every developer who has disciplined themselves in the ritualistic like art and science of Test Driven Development soon discovers that the collaborators on which a class under test depend introduce an additional layer of complexity to consider when writing your tests – and designing your APIs.
For example, consider a simple test against a class Car
which has an instance of class Engine
. Car
implements a start
method which, when invoked, calls the Engine
object’s run
method. The challenge here lies in testing the dependency Car
has on Engine
, specifically, how one verifies that an invocation of Car.start
results in the Engine
object’s run
method being called.
There are two ways of testing the above example of Car
, which in unit testing nomenclature is called the System Under Test (SUT), and it’s Engine
instance which is Car's
Depended-on Component (DOC). The most common approach is to define assertions based on the state of both the SUT and it’s DOC after being exercised. This style of testing is commonly referred to as State Verification, and is typically the approach most developers initially use when writing tests.
Using the above Car example, a typical State Verification test would be implemented as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class CarTest { private var _car:Car = null; [Before] public function setUp() : void { _car = new Car() } [Test] public function testStart() : void { _car.start(); assertTrue( _car.isStarted() ); assertTrue( _car.engine.isRunning() ); } [After] public function tearDown() : void { _car = null; } } |
Figure 1. CarTest, State Verification.
From a requirements perspective and therefore a testing and implementation perspective as well, the expectation of calling start
on Car
is that it will A.) change it’s running state to true, and B.) invoke run
on it’s Engine
instance. As you can see in Figure 1, in order to test the start method on Car
the Engine
object must also be tested. In the example, using the State Verification style of testing, Car
exposes the Engine
instance in order to allow the state of Engine
to be verified. This has lead to a less than ideal design as it breaks encapsulation and violates Principle of Least Knowledge. Obviously, a better design of Car.isStarted
could be implemented such that it determines if it’s Engine
instance is also in a running state; however, realistically, Engine.run
will likely need to do more than just set its running state to true; conceivable, it could need to do much, much more. More importantly, while testing Car
one should only be concerned with the state and behavior of Car
– and not that of its dependencies. As such, it soon becomes apparent that what really needs to be tested with regards to Engine
in Car.start
is that Engine.run
is invoked, and nothing more.
With this in mind, the implementation details of Engine.run
are decidedly of less concern when testing Car
; in fact, a “real” concrete implementation of Engine
need not even exist in order to test Car
; only the contract between Car
and Engine
should be of concern. Therefore, State Verification alone is not sufficient for testing Car.start
as, at best, this approach unnecessarily requires a real Engine
implementation or, at worst, as illustrated in Figure 1, can negatively guide design as it would require exposing the DOC in order to verify its state; effectively breaking encapsulation and unnecessarily complicating implementation. To reiterate an important point: State Verification requires an implementation of Engine
and, assuming Test First is being followed (ideally, it is), the concern when testing Car
should be focused exclusively on Car
and it’s interactions with its DOC; not on their specific implementations. And this is where the second style of testing – Behavior Verification – plays an important role in TDD.
The Behavior Verification style of testing relies on the use of Mock Objects in order to test the expectations of an SUT; that is, that the expected methods are called on it’s DOC with the expected parameters. Behavior Verification is most useful where State Verification alone would otherwise negatively influence design by requiring the implementation of needless state if only for the purpose of providing a more convenient means of testing. For example, many times an object may not need to be stateful or the behavior of an object may not always require a change in it’s state after exercising the SUT. In such cases, Behavior Verification with Mock Objects will lead to a simpler, more cohesive design as it requires careful design considerations of the SUT and it’s interactions with its DOC. A rather natural side-effect of this is promoting the use of interfaces over implementations as well as maintaining encapsulation.
For testing with Behavior Verification in Flex, there are numerous Mock Object frameworks available, all of which are quite good in their own right and more or less provide different implementations of the same fundamental concepts. To name just a few, in no particular order, there are asMock, mockito-flex, mockolate and mock4as.
While any of the above Mock Testing Frameworks will do, for the sake of simplicity I will demonstrate re-writing the Car
test using Behavior Verification based on mock4as – if for nothing other than the fact that it requires implementing the actual Mock, which helps illustrate how everything comes together. Moreover, the goal of this essay is to help developers understand the design concepts surrounding TDD with Behavior Verification and Mock Objects by focusing on the basic design concepts; not the implementation specifics of any individual Mock Framework.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public class CarTest { private var _car:Car = null; private var _mock:MockEngine = null; [Before] public function setUp() : void { _mock = new MockEngine(); _car = new Car( _mock ); } [Test] public function testStartChangesState() : void { _car.start(); assertTrue( _car.isRunning() ); } [Test] public function testStartInvokesEngineRun() : void { _mock.expects( "run" ); _car.start(); assertTrue(_mock.errorMessage(), _mock.success()); } [After] public function tearDown() : void { _mock = null; _car = null; } } |
Figure 2. CarTest, Behavior Verification approach.
Let’s go through what has changed in CarTest
now that it leverages Behavior Verification. First, Car's
constructor has been refactored to require an Engine
object, which now implements an IEngine
interface, which is defined as follows.
| public interface IEngine { function run() : void; } |
Figure 3. IEngine interface.
Note Engine.isRunning
is no longer tested, or even defined as, it is simply not needed when testing Car
: only the call to Engine.run
is to be verified in the context of calling Car.start
. Since focus is exclusively on the SUT, only the interactions between Car
and Engine
are of importance and should be defined. The goal is to focus on the testing of the SUT and not be distracted with design or implementation details of it’s DOC outside of that which is needed by the SUT.
MockEngine
provides the actual implementation of IEngine
, and, as you may have guessed, is the actual Mock object implementation of IEngine
. MockEngine
simply serves to provide a means of verifing that when Car.start
is exercised it successfully invokes Engine.run
; effectively satisfiying the contract between Car
and Engine
. MockEngine
is implemented as follows:
| import org.mock4as.Mock; public class MockEngine extends Mock implements IEngine { public function run() : void { record( "run" ); } } |
Figure 4. MockEngine implementation.
MockEngine
extends org.mock4as.Mock
from which it inherits all of the functionality needed to “Mock” an object, in this case, an IEngine
implementation. You’ll notice that MockEngine.run
does not implement any “real” functionality, but rather it simply invokes the inherited record
method, passing in the method name to record for verification when called. This is the mechanism which allows a MockEngine
instance to be verified once run
is invoked.
CarTest
has been refactored to now provide two distinct tests against Car.start
. The first, testStartChangesState()
, provides the State Verification test of Car
; which tests the expected state of Car
after being exercised. The second test, testStartInvokesEngineRun()
, provides the actual Behavior Verification test which defines the expectations of the SUT and verification of those expectations on the DOC; that is, Behavior Verification tests are implemented such that they first define expectations, then exercise the SUT, and finally, verify that the expectations have been met. In effect, this verifies that the contract between an SUT and its DOC has been satisfied.
Breaking down the testStartInvokesEngineRun()
test, it is quite easy to follow the steps used when writing a Behavior Verification test.
| [Test] public function testStartInvokesEngineStart() : void { // Set expectations on the mock; essentially, what // method the SUT is expected to invoke on it's DOC _mock.expects( "run" ); / / Exercise the SUT _car.start(); // Verify expectations have been met assertTrue(_mock.errorMessage(), mock.success()); } |
And that’s basically it. While much more can be accomplished with the many Mock Testing frameworks available for Flex, and plenty of information is available on the specifics of the subject, this essay quite necessarily aims to focus on the design benefits of testing with Behavior Verification; that is, the design considerations one must make while doing so.
With Behavior Verification and Mock Objects, design can be guided into existence based on necessity rather than pushed into existence based on implementation.
The example can be downloaded here.