Mock Objects and Distributed Testing

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).

Figure 1: The object testing problem

The Object Testing Problem

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).

Figure 2: Using mock objects to test objects in isolation

Using Mock Objects for Test Objects in Isolation

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).

Figure 3: Mock objects and the test code collaborate to conduct tests

Mock Objects and the Test Code Collaborate to Conduct Tests

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:

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).

Figure 4: Interfaces allow for mock and real implementations

Interfaces Allow for Mock and Real Implementations

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.

Figure 5: Direct instantiation of objects creates a problem when testing with Mock Objects

Direct Instantiation of Objects Creates a Problem when Testing with Mock Objects

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).

Figure 6: Using a factory to avoid directly instantiating objects to be mocked out

  1. // ...
  2.  
  3. public class RecipeImpl implements Recipe {
  4.  
  5. private Set ingredients;
  6.  
  7. private IngredientFactory ingredientFactory;
  8.  
  9. // ...
  10.  
  11. public void addIngredient( String name, Quantity amount ) {
  12.  
  13. Ingredient i = ingredientFactory.createIngredient( name, amount );
  14.  
  15. ingredients.add( i );
  16. }
  17.  
  18. // ...
  19. }

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

Figure 7: Using dependency injection to bind an object with a factory object

  1. // ...
  2.  
  3. public class RecipeImpl implements Recipe {
  4.  
  5. private IngredientFactory ingredientFactory;
  6. private Set ingredients;
  7.  
  8. // ...
  9.  
  10. // Using setter dependency injection
  11. public void setIngredientFactory( IngredientFactory f ) {
  12. ingredientFactory = f;
  13. }
  14.  
  15. // ...
  16.  
  17. public void addIngredient( String name, Quantity amount ) {
  18. Ingredient i = ingredientFactory.createIngredient( name, amount );
  19. ingredients.add( i );
  20. }
  21.  
  22. // ...
  23. }

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:

Distance Matters

When we work with distributed applications, there are new failure modes for our application:

There are also new sources of existing failure modes:

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. 

Figure 8: A simple web application

A Simple Web Application

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.

Figure 9: More realistic anatomy of a 'simple' web application

More Realistic Anatomy of a Simple Web Application

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.

Figure 10: Removing distribution to test correctness

fig 10: Removing distribution to test correctness

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.

Figure 11: Mocking out around a component while leaving in distribution

Mocking Out Around a Component While Leaving in Distribution

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:

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

Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.


secret