Mock Objects and Distributed Testing
By Brian Gilstrap, OCI Principal Software Engineer
August 2005
Introduction
In recent years, the development community has found unit testing (especially automated unit testing), invaluable in building reliable software. Mock objects have been a key technique for enabling automated unit testing of object-oriented software. However, when we expand the scope of testing into the realm of distributed systems, unique problems arise from the increased complexity. In this article we will briefly review mock objects. We will then discuss some of the problems that arise when extending the use of mock objects to distributed systems and some approaches that can help in managing the complexity.
Mock Objects
The basic idea behind mock objects came from the problems associated with testing object-oriented systems. When you have a set of collaborating objects, how do you test any one of them by themselves? (see Figure 1).
Mock objects provide a solution to this problem of independent testing. Looking at the example, we want to test Recipe
but don't want to test any of the other objects (yet). Doing this allows us to verify the correct behavior of Recipe
class by itself. This is useful in re-testing as the implementation of Recipe
changes and to track down bugs that arise due to interaction between objects. Unfortunately, the Recipe
class uses both the Ingredient
and Instruction
classes in its implementation. To enable independent testing, we replace the real implementations of Ingredient
and Instruction
classes with fake, or mock versions that implement the same interface as the real objects (see Figure 2).
When we implement the mock versions of Ingredient
and Instruction
, we write the mock implementations so they collaborate with the testing code. This allows us to verify that the Recipe
class interacts with the other classes at the right times and in the right ways (see Figure 3).
Testing with Mock Objects
Mock Object Principles
Since mock objects are a technique for building unit tests, it's not surprising that they share many of the same principles. In particular:
- Start small - Start with the smallest possible tests (e.g. testing single methods), to gain confidence in the implementation
- Isolate tests - Make tests independent from each other, so a change in one test does not alter or invalidate another
- Built upon success - Once the simplest tests are implemented, you can build up more sophisticated tests that leverage your confidence in lower layers of tests
- Make tests automated - If they aren't automated, tests won't be run regularly; human nature and tight schedules will take their toll otherwise
In addition to these principles of unit testing, there is one other guideline specific to mock objects: disguise the mock objects. When testing using mock objects, the tested class should not realize it is interacting with mocks. This is necessary to avoid having different code when testing than when in production. The whole point is to test the actual production code.
Disguising your Mock Objects
When using mock objects in Java development, there are two key techniques to disguise them from the production code: use interfaces and use dependency injection.
Using Interfaces
When you are designing your system, you should use interfaces to represent any part of the system you want to be able to test independently. If you don't use interfaces, you will run into trouble trying to substitute a mock object for the real one. By using interfaces, you can use a mock object in your tests and use the real implementation in production (see Figure 4).
Using Dependency Injection
After all the proper interfaces have been created and mock objects have been implemented, you still need to allow for the substitution of mock objects in your tests. If the class you want to test explicitly constructs objects (see Figure 5), it is difficult or impossible to use your mock objects when running tests. To get around this, you should avoid directly creating objects in your code.
You can avoid this problem with a factory class. The factory class gives the Recipe
class an indirect means of creating Ingredient
objects (see Figure 6).
- // ...
-
- public class RecipeImpl implements Recipe {
-
- private Set ingredients;
-
- private IngredientFactory ingredientFactory;
-
- // ...
-
- public void addIngredient( String name, Quantity amount ) {
-
- Ingredient i = ingredientFactory.createIngredient( name, amount );
-
- ingredients.add( i );
- }
-
- // ...
- }
Then, you can use dependency injection to provide the Recipe
with an IngredientFactory
(see Figure 7). This allows tests to provide a factory that produces mock ingredients for tests that need them, and a normal factory in production (or when testing other parts of the system). and dependency injection
- // ...
-
- public class RecipeImpl implements Recipe {
-
- private IngredientFactory ingredientFactory;
- private Set ingredients;
-
- // ...
-
- // Using setter dependency injection
- public void setIngredientFactory( IngredientFactory f ) {
- ingredientFactory = f;
- }
-
- // ...
-
- public void addIngredient( String name, Quantity amount ) {
- Ingredient i = ingredientFactory.createIngredient( name, amount );
- ingredients.add( i );
- }
-
- // ...
- }
Mock Objects for Distributed Systems
When we move up to distributed systems, the testing problem becomes more difficult. Our code may now be invoked by or invoke remote entities, and this complicates the testing problem. Real-world examples of distributed entities to test or mock out include:
- Databases
- EJBs
- Servlets
- CORBA servants
- RMI objects
- Java Connector Architecture (JCA) services
- etc.
Distance Matters
When we work with distributed applications, there are new failure modes for our application:
- Communication failure - networking problems can prevent us from communicating with a remote object, something that can't happen with a local object in the same JVM
- Service/component failure - the remote service/component can fail, disappearing while we are running
- Transport errors - as good as they are, marshalling and unmarshalling libraries and underlying transport systems do occasionally exhibit bugs
There are also new sources of existing failure modes:
- Race conditions - these are more likely to manifest themselves in distributed systems, because of the longer times involved in invoking remote objects/services
- Deadlock - these are not any more likely to occur than in monolithic systems, but when they occur they are generally much harder to diagnose
- Bad dependency resolution - trying to connect to a missing object, or trying to use one object as if it were another is much more common in distributed systems
Because of these new failure modes (or the increased chance of seeing them), testing of distributed systems is even more important than in monolithic systems. It is also made harder by the distributed nature of the application.
To make our distributed application robust, we need to account for these sorts of failues. Making our system tolerant of these sorts of failures requires that we implement solutions for them (such as re-trying requests) and that we then test that code to verify it functions as expected.
Distributed Testing Tradeoffs
When we test distributed systems, we have to make tradeoffs. Should we use pre-determined requests and (expected) responses so we can test with distributed calls? Should we remove distributed calls in order to perform a sophisticated test of the logic of the component with local mocks of the remote objects? Should we write a long-running random test in hopes of finding subtle problems? Each kind of test is useful, and the test we choose should be tailored to what we are trying to test.
In general, we can most easily test complicated logic by eliminating the remote calls. This allows our testing code, the mock objects replacing remote objects, and the tested object/component to reside in one place. This in turn allows us to more easily orchestrate the logic of the test to make sure we know whether the test was successful or failed.
Similarly, if we want to test the timing issues and transport code for a distributed application, we have to use remote mock objects. In this case, it is simpler to use a small number of fairly simple request/response test cases and record the activity that takes place. After the 'active' part of the test is complete, we can collect the record of each component's activity and reconcile them with the expected results. It is especially nice if we can encode something to identify the test in the invocation of the test (in a string that is normally the name of an object, for example). This simplifies the reconciliation, making it easier to identify failed tests.
Example: WebApp
As an example for discussion, let's look at a simple WebApp. The WebApp is accessed via a browser and in turn the Servlet in the WebApp talks to a database via JDBC. Many people would draw a diagram of the application like that in Figure 8.
However, this picture greatly over-simplifies the set of technologies involved. A more accurate (though still simplified) diagram would be more like the one in Figure 9.
So, given the relatively complex nature of the application, how can we implement unit tests for the application?
Testing Basic Correctness
To test basic correctness, we should eliminate distribution, mocking out the browser and the database. Along with that, we should mock out the Servlet container, so we can assure ourselves that bugs in the servlet container are not the cause of test failures. This leaves us with a situation like that shown in Figure 10.
By removing the distribution and many of the transport details, we can focus on the basic logic of the servlet. This sort of configuration works well for testing both expected and unexpected situations (such as incorrect inputs).
Testing with Transport included
When we are checking for timing issues (race conditions or deadlocks), we generally want to have a true distributed test. We mock out the remote components (in this case the browser and the database), but leave the distributed calls and (un)marshalling libraries in place (see Figure 11). In this situation, we generally must also use the servlet container in place, in order to assure that the interaction with the transport and lower-level libraries is realistic.
When we do this, we need to correlate the requests from our mock browser, the invocations of the mock database from the servlet, and the results that the mock browser receives. By correlating the information, we can determine whether the test was successful or not. To do this, we can:
- Create pre-determined tests in a pre-determined order, so the mock browser and mock database don't need to communicate to detect an error. This is particularly good for tests of race conditions and deadlocks, since things run at 'full speed'.
- Embed in the calls to the servlet enough information that the mock database can determine from the request coming to it the proper information to return. The mock browser and mock database don't need to directly communicate, because the information the mock database needs appears inside the requests from the mock browser. The servlet ends up passing them through to the mock database none the wiser. However, this can be difficult to achieve, since the servlet may alter the incoming request, may have validation rules that prevent us from embedding information in the request, or may only use portions of the request in making calls to the mock database.
- The mock browser and mock database can communicate out-of-band (not via the servlet) to orchestrate the calls to the servlet. This is good when checking for multi-threading errors and transport/marshalling bugs.
Conclusion
Testing of distributed systems will never be easy. It requires careful design of the test to make sure we isolate the intended pieces of the system and achieve a clear result. Still, mock objects are a powerful technique for testing object-oriented and component-oriented systems, and it helps that mock objects can be applied to testing distributed systems.
In this article, we've barely scratched the surface of distributed mock objects. Hopefully, it has been enough to help you apply mock objects in testing distributed applications.
References
- The Original Paper
- [1] Endo-Testing: Unit Testing with Mock Objects
http://www.connextra.com/aboutUs/mockobjects.pdf
- [1] Endo-Testing: Unit Testing with Mock Objects
- General Sites
- [2] Mockobjects.com, a clearinghouse website for mock objects in Java
http://www.mockobjects.com
- [2] Mockobjects.com, a clearinghouse website for mock objects in Java
- Articles
- [3] A previous SETT Article with a detailed example of using mock objects
- [4] A good article on unit testing with mock objects
http://www-106.ibm.com/developerworks/library/j-mocktest.html
- Frameworks
- [5] A general mock objects framework
http://www.mockobjects.com - [6] A mock objects framework using dynamic proxies
http://www.easymock.org - [7] HTTPUnit, a framework to mock out the servlet container and browser
http://httpunit.sourceforge.net - [8] JDBCUnit, a framework to mock out a database accessed via JDBC
http://jdbcunit.sourceforge.net - [9] Cactus, a framework for testing server-side code
http://jakarta.apache.org/cactus/index.html
- [5] A general mock objects framework
- Books
- [10] Pragmatic Unit Testing (Pragmatic Programmers) - by Andy Hunt, Dave Thomas
- [11] Unit Testing in Java: How Tests Drive the Code - by Johannes Link
Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.