Designing Testability with Mock Objects

Designing Testability with Mock Objects

By Mario Aquino, OCI Software Engineer 

June 2003


Introduction

Unit testing is a critical aspect of software development. Many development processes, including extreme programming (XP), list unit test writing as a critical step in software development.

The JUnit testing framework has revolutionized the ease with which Java developers can write and automate the running of unit tests. However, many Java developers still have trouble integrating unit test writing into their regular programming activities; some don't think they have enough time to write unit tests, or they think their systems are too complicated to test, and some may not even know how best to write testable software.

Systems large and small can have design dependencies that make testing one piece of software overly complicated because of all the other aspects of a system that may need to be initialized or setup in a special way just so a test can be run.

Unit testing can be made easier by designing code that is easy to test.

That seems simple, doesn't it? So, how do we design code that is easy to test, code that can be tested in isolation without having to depend on the availability of external systems like databases, EJB containers, servlet containers, etc?

One way is by designing software that makes use of interfaces to represent other components in a system. Through this approach, aspects of a system can be "mocked" out so that the component's ability to be tested does not require the availability of services that might be available only in production.

Using examples, this article will attempt to describe an approach to designing testable software using interfaces, mock objects, and JUnit.

Testability & Design

We should start by looking at a class called TransactionManager that, while looking totally functional, is designed in a way that does not promote easy testability.

The class has some properties that are loaded in a static initializer. Also in the static initializer is the instantiation of the instance for this class, which happens to be designed as a singleton (the private constructor, and the 'getInstance()' method should give this away).

This class has an initialize method that gets a connection to a database using a standard 'DriverManager.getConnection()' call, sets autocommit on the connection to false, and creates a dynamic proxy for the connection.

The proxy is written so that any calls to either the commit or rollback methods of the 'Connection' will not make it to the connection instance.

Finally, the class has 'commit()' and 'rollback()' methods that seem simple enough.

There are several aspects about this design that make unit testing complicated.

First, this class makes explicit calls to load its properties as well as get a connection to a database. This design forces the properties file and a running database server to be available in order for any unit testing to run. This dependency is not desirable but neither is it unavoidable as we will see later in the article in the redesign of the structure.

Additionally, the properties for this class are loaded in a static initializer which introduces a complication related to the order in which this class is loaded at runtime in relation to the unit test that tests this class; if we wanted to "fake-out" or substitute properties that this class will use in the unit test, any code to support this would have to execute before TransactionManager was loaded by the class loader.

Furthermore, this class is a Singleton, which is a design pattern that can easily cause trouble by virtue of being essentially the dreaded "global variable". A specific problem this causes, in conjunction with the class' dependency on the availability of a database, is that its dependencies apply to any class that uses it or more specifically any unit tests for classes that use TransactionManager.

The design dependencies exhibited by this class make not only writing but also executing a unit test for it complicated. It is likely that one reason developers don't write as many unit tests as are needed to promote code quality is that bad design decisions make the job of writing unit tests a chore.

What You Really Want Is...

We have identified some factors that make unit testing this class more complicated than it needs to be. What we want is to change the design in such a way as to eliminate dependencies on a runtime environment and make unit test writing and execution easier. One way to achieve this is through the use of interfaces and "Mock" objects in the design of the system. Interfaces provide a way to create types in Java where fields and methods can be defined without implementations. Mock objects can make use of those types to replace the real implementation of system services so that components can be unit tested by themselves regardless of runtime dependencies they may have. Mock objects are simply classes that implement interfaces and are passed to other classes who only know the interfaces that the mocks implement.

There are two styles or approaches for mock objects, namely static and dynamic, as well as several open source toolkits that attempt to make creating and working with mocks easier. Static mock objects are regular Java classes that implement some interface and keep track of which methods are called as well as any parameters that may have been passed into them. Static mocks can either be hand coded (which may be a chore depending on the pattern used for the mock implementation) or they can be generated by some tool that reads the methods of an interface and outputs a source file for the Mock object. Dynamic mocks can either be implemented as proxies (java.lang.reflect.Proxy) or through aspect oriented programming (AOP) techniques like byte code manipulation to intercept calls to real implementations by objects that just record method calls and their parameters without executing the real called method.

Mock objects

Before going any further, lets take a look at what a mock object looks like and then decide which strategy to employ to solve our testability problem with the TransactionManager. Given the following interface for 'Foo':

  1. import java.util.List;
  2.  
  3. public interface Foo {
  4. List doSomeFoo(String name, int number) throws TooMuchFooException;
  5. }

This is what the mock for the interface would look like:

  1. import java.util.List;
  2.  
  3. public class MockFoo implements Foo {
  4.  
  5. public boolean doSomeFooCalled;
  6. public List doSomeFooReturn;
  7. public Throwable doSomeFooException;
  8. public String doSomeFooName;
  9. public int doSomeFooNumber;
  10. public List doSomeFoo(String name, int number) throws TooMuchFooException {
  11. doSomeFooCalled = true;
  12. this.doSomeFooName = name;
  13. this.doSomeFooNumber = number;
  14. if (doSomeFooException != null) {
  15. if (doSomeFooException instanceof TooMuchFooException)
  16. throw (TooMuchFooException) doSomeFooException;
  17. if (doSomeFooException instanceof RuntimeException)
  18. throw (RuntimeException) doSomeFooException;
  19. if (doSomeFooException instanceof Error)
  20. throw (Error) doSomeFooException;
  21. throw new RuntimeException();
  22. }
  23. return this.doSomeFooReturn;
  24. }
  25. }

This mock object has public fields that track whether the method has been called, what any parameters passed into it were, any exception that the method may need to throw, and a return value that would be appropriate for a call. In a unit test where this mock was used, the unit test method would set the public field for the return value and set the exception field if the test wanted to observe how the method being tested reacted to exceptional conditions. The test method would pass the "configured" mock object directly to the method being tested or put the mock object in a location that the method being tested would retrieve it. After the method being tested returned, the unit test could make assertions on the values that the mock object showed through its public fields. This is shown below:

  1. import junit.framework.TestCase;
  2.  
  3. import java.util.ArrayList;
  4.  
  5. public class TestUserOfFoo extends TestCase {
  6. public void testUserOfFoo() {
  7. MockFoo mock = new MockFoo();
  8. mock.doSomeFooReturn = new ArrayList();
  9. UserOfFoo fooUser = new UserOfFoo();
  10. fooUser.useFoo(mock);
  11. assertTrue(mock.doSomeFooCalled);
  12. assertEquals("somename", mock.doSomeFooName);
  13. assertEquals(123, mock.doSomeFooNumber);
  14. }
  15. }

In this simple test case example, the class we are testing (UserOfFoo) defines a method ("useFoo()") that takes a Foo instance as a parameter. The test case makes assertions about whether the "doSomeFoo()" method will be called by the UserOfFoo object, as well as the parameters that the test case expects the UserOfFoo instance to pass into the "doSomeFoo()" method. The Foo in this example could represent some service like a database or an Enterprise Java Bean that the class being tested will rely on in production but that need not be around in order to unit test the logic of the component (in this case the UserOfFoo class).

Taking a Closer Look...

The MockFoo object implemented the Foo interface in the example above, but for our original TransactionManager example to be able to use mock objects, we will need to move a few things around. First we'll tackle the database dependency. The TransactionManager.initialize()method makes a call to DriverManager.getConnection(), passing it a database URL, username, and password (that were retrieved from a properties file loaded in the static initializer). This call represents our database dependency and we can refactor the code to deal with it in several different ways:

  1. The DriverManager.getConnection() call can be moved into it's own method, perhaps one called getConnection() that takes String parameters for the database URL, the username, and the password and returns an object that implements the Connection interface. As long as the getConnection() method has protected accessibility, the unit test can create an inner class that extends TransactionManager and overrides that method. The overridden method in the unit test would have an assertStatement in it to test the parameters passed in. The overridden method would also have to return a Connection object, but because Connection is an interface we would create a class called MockConnection that implements the Connection interface and follows the pattern shown in the MockFoo class above.
  2. An alternate design would be to have the initialize() method take a Connection object as a parameter, removing the need for TransactionManager to retrieve a Connection directly. This solution might make more sense for a class that wasn't a Singleton, though, because it would mean that the Singleton either required a Connection object to be passed into the method that returns the single instance of the class or that users of the instance would need to pass in a Connection object to some initialization method before using TransactionManager's regular services. This doesn't seem like a good fit in this case.
  3. Another option is to move the DriverManager.getConnection() call into a totally separate structure. Perhaps a Singleton called ConnectionFactory that has a createConnection() method taking the parameters that DriverManager needs. Ideally this would be defined as an interface that would be implemented by an object that has the "real" implementation (the one that calls DriverManager.getConnection()) and by a mock object that returns a MockConnection instance.

Given these possibilities, number 3 sounds like the best choice. For this option to work, we should look at how TransactionManager is designed as a Singleton and refactor it to support substitution by a mock object.

Singletons and Unit Testing

TransactionManager is a functional Singleton because it has a private constructor and an instance is created in the static initializer of the class (which is only executed once, when the class is first loaded). The problem with this design is that we can't change out the real implementation of a Singleton with a mock implementation very easily. It would be better if this class was more of an "operational" Singleton; a class that would have a single instance available but that is enforced by convention rather than by design.

Singletons are "global" variables because only one of them exists and is globally accessible in a system at a time. This can also be supported through a convention of separating the classes that hold references to objects that are Singletons from the Singletons themselves.

Refactoring the TransactionManager

For the ConnectionFactory suggested by number 3 above, we would want to use a new way to configure TransactionManager so that we can provide it with ConnectionFactory reference in its constructor and then use TransactionManager as an operational Singleton. This will remove the static reference to TransactionManger to a new class that we will call GlobalTransactionManager, and it will look like this:

  1. public class GlobalTransactionManager {
  2. private static TransactionManager instance;
  3.  
  4. public static void set(TransactionManager manager) {
  5. instance = manager;
  6. }
  7.  
  8. public static TransactionManager get() {
  9. return instance;
  10. }
  11. }

As part of this change, we need to make TransactionManager into an interface and change the class we have written to implement that interface. The benefit of this design is that the TransactionManager can be mocked-out in a unit test while in production the real instance would be used. One caveat to this approach is that it would be the responsibility of some system setup class to remember to create an instance of the real TransactionManagerImpl and pass the reference to the GlobalTransactionManager. This can be mitigated by changing the get() method above to check whether the static instance field is null and if it is to create a new instance of TransactionManagerImpl and keep a reference to that instance internally. In either approach, it is the responsibility of the unit test to "setup" it's environment by creating mock objects where they are needed and passing the instances to "operational" Singleton classes like GlobalTransactionManager.

Figure 1

The classes in the "com.ociweb.jnb.ex2" package represent our first refactoring (ConnectionFactory, ConnectionFactoryImpl, GlobalTransactionManager, MockConnectionMockConnectionFactory, TestTransactionManagerImpl, TransactionException, TransactionManagerTransactionManagerImpl), in which we have eliminated the database dependency for the unit test and changed the Singleton pattern to support mocking. With this much in place, we can write a unit test to check a few things about the class, like that it sets autocommit on the Connection object it receives to false, that the commit and rollback methods call commit and rollback on the Connection it keeps a reference to. We also want to test the proxy behavior. The getConnection() method returns a Connection object that is wrapped by a proxy. The proxy is written so that any calls on it to the commit() and rollback() methods will do nothing (the idea here is to force calls to the TransactionManager commit() and rollback() methods). The unit test we have written tests all of these things.

  1. package com.ociweb.jnb.ex2;
  2.  
  3. import junit.framework.TestCase;
  4.  
  5. import java.sql.Connection;
  6. import java.sql.SQLException;
  7.  
  8. public class TestTransactionManagerImpl extends TestCase {
  9. private TransactionManagerImpl manager;
  10. private MockConnection mockConnection;
  11. private MockConnectionFactory mockConnectionFactory;
  12.  
  13. public void setUp() {
  14. mockConnection = new MockConnection();
  15. mockConnectionFactory = new MockConnectionFactory();
  16. mockConnectionFactory.createConnectionReturn = mockConnection;
  17. }
  18.  
  19. public TestTransactionManagerImpl(String name) {
  20. super(name);
  21. }
  22.  
  23. public void testTransactionManagerInitialization() {
  24. manager = new TransactionManagerImpl(mockConnectionFactory);
  25. assertTrue(mockConnectionFactory.createConnectionCalled);
  26. assertTrue(mockConnection.setAutoCommitCalled);
  27. assertEquals(false, mockConnection.setAutoCommitAutoCommit);
  28. }
  29.  
  30. public void testCommitSuccess() throws Exception {
  31. manager = new TransactionManagerImpl(mockConnectionFactory);
  32. manager.commit();
  33. assertTrue(mockConnection.commitCalled);
  34. }
  35.  
  36. public void testCommitFailure() throws Exception {
  37. mockConnection.commitException = new SQLException("Could not commit!");
  38. manager = new TransactionManagerImpl(mockConnectionFactory);
  39. try {
  40. manager.commit();
  41. fail("The TransactionManager should wrap an SQLException in a TransactionException");
  42. } catch (TransactionException e) {
  43. assertTrue(mockConnection.commitCalled);
  44. assertTrue(e.getCause() instanceof SQLException);
  45. assertEquals("Could not commit!", e.getCause().getMessage());
  46. }
  47. }
  48.  
  49. public void testRollbackSuccess() throws Exception {
  50. manager = new TransactionManagerImpl(mockConnectionFactory);
  51. manager.rollback();
  52. assertTrue(mockConnection.rollbackCalled);
  53. }
  54.  
  55. public void testRollbackFailure() throws Exception {
  56. mockConnection.rollbackException = new SQLException("SQL Failure!");
  57. manager = new TransactionManagerImpl(mockConnectionFactory);
  58. try {
  59. manager.rollback();
  60. fail("The TransactionManager should wrap an SQLException in a TransactionException");
  61. } catch (TransactionException e) {
  62. assertTrue(mockConnection.rollbackCalled);
  63. assertTrue(e.getCause() instanceof SQLException);
  64. assertEquals("SQL Failure!", e.getCause().getMessage());
  65. }
  66. }
  67.  
  68. public void testConnectionProxy() throws Exception {
  69. manager = new TransactionManagerImpl(mockConnectionFactory);
  70. Connection proxy = manager.getConnection();
  71. assertFalse("The connection commit has not been called yet", mockConnection.commitCalled);
  72. proxy.commit();
  73. assertFalse("The connection commit has still not been called", mockConnection.commitCalled);
  74. assertFalse("The connection rollback has not been called yet", mockConnection.rollbackCalled);
  75. proxy.rollback();
  76. assertFalse("The connection rollback has still not been called", mockConnection.rollbackCalled);
  77. assertFalse("The connection close has not been called", mockConnection.closeCalled);
  78. proxy.close();
  79. assertTrue("The proxy caused the real connection.close() method to be called", mockConnection.closeCalled);
  80. }
  81. }

Final Refactoring

Figure 2

Our final refactoring in the "com.ociweb.jnb.ex3" package (MockPropertiesManager, PropertiesManager, PropertiesManagerImpl, TestTransactionManagerImplTransactionManagerImpl) is to eliminate the Properties file dependency, so that we can test the retrieval of properties behavior without forcing the real properties file to be available to the unit test. It may seem trivial to refactor a dependency on a properties file, but that dependency would cause a unit test to fail if that file were moved to a new location. The purpose of the unit tests is not to indicate when arbitrary external dependencies break but rather to identify real problems in the logic of a unit of code. The redesign of the properties retrieval logic introduces a new structure called the PropertiesManager that will push out the responsibility of loading a properties file. The new design is easily mocked so that the unit test can determine that the class is asking for and using the right properties.

Patterns for Testable Software

We have seen some recurring patterns in the refactorings we did to TransactionManager, namely moving functionality that represents a runtime dependency into a totally separate structure by defining an interface and creating both a real implementation and a mock implementation to be used by the unit test. The new type can either be accessed as a Singleton (if it is appropriate) or by passing a reference to the type to a method in the class that needs it. In the latter pattern, the responsibility of knowing what or where to get a useful type is moved out of the class being tested and into a "setup" class (one that knows the needs of the class being tested). This pattern can sometimes border on breaking encapsulation depending on how it is used. At the same time, however, there is a natural tension between open design to promote testability and exposing more of the workings of a class than should be to maintain encapsulation and it is up to a developer (or team) to decide how to balance these.

Static vs. Dynamic

The beginning of this article mentions static and dynamic mocks, though up to this point all the code examples have used static mocks. Dynamic mock objects are those for which no formal class needs to be written or compiled in order for a "mock" instance of an object to be used where a real implementation might otherwise be. There are two approaches for dynamic mock objects: through proxies and through Aspect Oriented Programming (AOP) techniques. Proxies are objects created at runtime to wrap method calls to a given interface. Like the static mock object approach described above, they require that the objects they wrap implement an interface. An advantage of proxies is that they require less code to be written for the mock object while still supporting all the features of static mocks. However, a major drawback of proxies is that all method interaction is done with strings; where in static mocks a concrete method name is defined in the mock object class file, the dynamic mock method names are referenced via a String containing the method name. This can cause a problem when the name of a method is changed and that change is not reflected in the string used by the dynamic mock. Refactoring tools (specifically method renaming) are becoming available in more and more Java developers toolkits. Unless refactoring tools are smart enough to catch string references that may represent a method name, it is likely that dynamic mocks will break more often than static mocks, for which the compiler at least will catch a discrepancy between the spelling of a method in an interface and the name of a method that appears in a static mock class that implements that interface.

AOP seems like a better approach to dynamic mocks because you are not restricted to mocking only classes that implement an interface. AOP mock frameworks would work with any object in Java, allowing method calls to be intercepted by a "mock" advisor that would record the method being called and the parameters passed to it. Because AOP can be used to intercept any method call, the approach of letting ease of testing drive software design could be thrown out the window since all methods/objects are easy to mock or intercept. While this notion might seem seductive, designing easily testable software has a side benefit of being well organized, thought-out, and easy to understand.

Finally, there are a number of framework implementations for supporting unit testing with mock objects, both for generating static mock objects and dynamic mocks. A review of those frameworks, their capabilities, and their shortcomings is beyond the scope of this article. Some of these frameworks include MockMaker, EasyMock, and DynaMock (See References section below for links). While none of these tools were used to generate the static mocks for this article, it doesn't take long to realize that hand-coding static mock objects is tedious and should probably be delegated to a code generation tool.

Conclusion

Unit testing is a valuable, though sometimes neglected, component of software development. While some components are easier to test than others, all unit testing should be done in isolation separating individual code units from system dependencies so that unexpected behavior can be identified and hopefully corrected. This article has touched on several aspects of mock objects, how they can be used to promote the design of testable software, as well as approaches for implementing them in an overall system testing strategy.

References

Software Engineering Tech Trends is a monthly publication featuring emerging trends in software engineering.

© Object Computing, Inc. 1993, 2019. All rights reserved.

secret