The Mock Object Pattern
The Mock Object Pattern
Contextual Forces
Motivation
To break a dependency, and in so doing make the dependant object more easily testable.
We prefer to test one behavior at a time. Tests which test many behaviors at once are complex, and do not give very valuable information when they fail.
A tests must always test all those things which are not under its control, and therefore we must bring under the test's control all those things which we do not wish to test.
- A test should have a single reason to fail. If we do not break dependencies, then a test can fail when one or more of the dependencies fail, even though the thing we want to test is working fine.
- A test should run fast, so it can be run frequently. A test with heavyweight dependencies cannot run fast.
- A test that requires a lot of objects to be instantiated will tend to be complex. Tests should be kept as simple as possible.
Encapsulation
The pattern isolates the class being tested from an entity it depends upon.
Procedural Analog
Often in creating procedural systems we would follow the "top down" model, which later became known as "programming by intention". In this process, a function would be coded, but any dependency functions would be "stubbed out" at first, returning a default value or null. These dependency functions could easily be termed "mocked out".
In procedural code, we eliminate these stubs as we implement the functions. In mocking, we retain the mock object for testing, but they can also be used in incremental development.
Non-Software Analog
When automotive manufacturers test their cars for crash safety, they need to eliminate a dependency; the driver. Putting an actual driver in the car, and then crashing it, would be too risky.
So, they replace the driver with a mock, called a crash test dummy. This allows them to crash the car in a realistic way, without involving an actual person.
- The dummies are positionable, to allow the testers to test different seating positions, test front and side airbags, etc...
- The dummies are usually equipped with "shock sensors" that allow the testers, after the fact, to measure what impact forces have effected the dummy, and would therefore effect a person who would be in an actual crash.
These are analogies to mocks objects in that mocks can be made to be conditionable and inspectable.
Implementation Forces
Example
Mocks can be hand-crafted, or created by an automated tool. Here is an example of a hand-crafted mock.
The simplest form of a mock is one that returns a hard-coded value, or a value that is set via its constructor:
This sort of mock is not conditionable (the test cannot change the return value) and thus cannot be used to test various scenarios. Also, it is not inspectable and therefore cannot be used to test workflows. But it may be adequate for a simple scenario. A slightly more capable mock would have methods for conditioning and inspection.
Note: The ClassUnderTest would hold the MockObject in an upcast to Dependency, which means it can only use the mock in the way it will use the real dependency. The Test, however, holds its reference to MockObject as MockObject, which gives it access to the additional methods which allow for conditioning and inspection.
There are tools for automating the production of mocks, which can accomplish these two critical aspects in different ways. See Cost-Benefit below for examples.
Pseudo Code
public class ClassUnderTest { private Dependency myService; public ClassUnderTest(Dependency aService) { myService = aService; } public ret M1(par) { val = myService.getState(); rval = val + par; //or some algorightm return rval; } public void m2(par) { val = getState(); // Note void return } } public class Test { private ClassUnderTest myCUT; private Dependency mockObject; private val expectedReturn; public void setUp() { expectedReturn = someKnownValue; mockObject = new MockObject(); myCUT = new ClassUnderTest(mockObject); } public void testM1(){ mockObject.setState(testValue); result = myCUT.m1(knownParameter); Assert.areEqual(expectedReturn, result, "should get known result given test parameter and value"); } public void testM2(){ myCUT.m2(knownParameter); String result = mockObject.checkCalls(); Assert.areEqual("getState() called", result, "should use dependency via getState() methhod"); } } public class Dependency { public val getState() { // unknown, uncontrollable behavior return mysteryVal; } } public class MockObject : Dependency { private val myState; private String calls; public val getState() { return myState; calls += "getState() called"); } public void setState(val newState) { myState = newState; } public String checkCalls() { return calls; } }
One potential difficulty with creating mocks through direct inheritance, as shown above, is that it means the original dependency object will be instantiated at test-time, even though the ClassUnderTest will not be given a reference to it. This is due to the inherent behavior of the class loader; base classes are always instantiated to support the derived class when it is insstantiated.
If this causes concern (the original dependency makes a heavyweight connection to a resource or is otherwise undesireable for testing), then an Interface or Abstract class should be used to represent the dependency, with the Mock as a totally seperate implementation:
Another potential difficulty exists where the dependency cannot be changed (to extract an interface), and cannot be directly subclassedsubclassed (it is final or sealed). In this case, it must be wrapped in order for the ClassUnderTest to be testable:
This requires the ClassUnderTest to be altered to hold a DependencyWrapper, rather than the original dependency type.
These two techniques can be combined, to avoid instantiating the RealDependency at test-time, without changing it (or attempting to subclass it):
Questions, concerns, credibility checks
- The mock ususally must be conditionable and inspectable, whereas the object it replaces likely is neither, in well-encapsulated systems. Therefore, the mock must have additional capabitlites. When creating hand-crafted mocks through subclassing, additional methods are created to allow for conditioning and inspection (the setState() and checkCalls() methods in our pseudo code example above). With automated tools, such as NMock and EasyMock, usually a second object called a "control" is presented as a sort of "remote control" to allow for this additional behavior.
- The mock must be inserted into the Class Under Test somehow, replacing the real dependency. In our example, the Class Under Test took its dependency via the constructor. This is called dependency injection, and can be a good solution. However the use of object factories and the Factory Method pattern is sometimes preferable.
- The use of the Factory Method is often termed [Endo-Testing.]
- An inspectable mock allows for the test of a workflow. If, for instance, we wish to write a test for the behavior associated with the method m2(pqr):void in ClassUnderTest above, we note that such a test would be impossible due to the void return from the method. We can test that m2 does what it is supposed to do by examining, after the fact, what happens to the dependency if said dependency is represented by an inspectable mock object. However, this technique should not be overused as it couples the test to this particular implementation, and thus could impede future refactoring. We only want to test workflows when the delegation itself is a behavior that needs to be tested on its own (as is usually true with void returns).
Options in implementation
Whether a mock is hand-crafted or generated by a tool, it is usually considered as a seperate object from both the Class Under Test and the Test itself. However, if the issue is simple, one can use the test itself as a 'shunt':
public class Test : Dependency{ private ClassUnderTest myCUT; private val expectedReturn; public void setUp() { expectedReturn = someKnownValue; myCUT = new ClassUnderTest(this); } public void testM1(){ shuntState=testValue; result = myCUT.m1(knownParameter); Assert.areEqual(expectedReturn, result, "should get known result given test parameter and value"); } public void testM2(){ myCUT.m2(knownParameter); Assert.areEqual("getState() called", calls, "should use dependency via getState() methhod"); } // Shuting State and Methods private val shuntState; private String calls; public val getState() { return shuntState; calls += "getState() called"); } }
This is called "self shunt."
Because the test 'is' the mock, no conditioning or inspection methods are needed. The downside, of course, is that the test's cohesion is weakened. It also requires making the test implement the dependency interface, which may not be possible in some frameworks.
A third alternative is to make the mock an 'inner class' of the test. This reduces communication overhead, because an inner class can access the state of its enclosing class.
This is called an "inner shunt".
public class Test { private ClassUnderTest myCUT; private val expectedReturn; private val shuntState; private String calls; public void setUp() { expectedReturn = someKnownValue; myCUT = new ClassUnderTest(new Shunt()); } public void testM1(){ shuntState=testValue; result = myCUT.m1(knownParameter); Assert.areEqual(expectedReturn, result, "should get known result given test parameter and value"); } public void testM2(){ myCUT.m2(knownParameter); Assert.areEqual("getState() called", calls, "should use dependency via getState() methhod"); } private inner class Shunt : Dependency { public val getState() { return shuntState; calls += "getState() called"); } } }
Consequent Forces
Testing issues
Mock Objects are great enablers of testing and good design, because:
- Dependencies are reduced/eliminated.
- Tests will run faster (if, for instance, we do not hit the actual database)
- Once we mock out everything we can, if testing is still difficult this is likely revealing a weakness in design.
Cost-Benefit (gain-loss)
Hand-crafted mocks can add maintainence burdens to a team, because there are more classes to create, maintain, and track. Also, because of this, teams will sometimes create mocks that are overly simple, which can lead to unrealistic testing scenarios.
These problems can be greatly ameliorated by automating the production of mocks. Some example are:
Visit [www.mockobjects.com] for more options.