My pages Projects Communities java.net
 

JMockit: a Java class library for developer testing with "mock methods"

Tutorial

Contents

Developer testing and test isolation

Software tests written by the developers themselves, to test their own code, is often very important for succesful software development. Such tests are usually written with the help of a testing framework, the most popular of which is JUnit.

Developer tests can be put in two main categories:

Unit tests, intended to test a class in isolation from the rest of the system. Integration tests, intended to test system operations that encompass a class and its dependencies (other classes with which the class under test interacts).

Even though integration tests include the interaction between multiple classes, particular tests may not be interested in exercising all components, layers, or subsystems involved. The ability to isolate the code under test from irrelevant parts of the system is therefore generally useful.

Testing with mock objects

A common and powerful technique for testing code in isolation is the use of "mock objects". A mock object is an instance of a class specifically implemented for a single test or set of related tests. This instance is passed to code under test to take the place of one of its dependencies. Each mock object behaves in the way expected by both the code under test and the tests that use it, so that all tests can pass. That, however, is not the only role mock objects usually play. As a complement to the verifications (assertions) performed by each test, the mock class itself can encode additional assertions.

Mocks are most useful for unit tests, but can also be used for other kinds of developer tests, such as integration tests. For example, you may want to test a presentation layer class along with its interactions with other classes in the same layer, without actually depending on the result of calls made to code in other application layers, such as the domain or persistence layers.

Tools for testing with mock objects

Existing tools for testing with mock objects include EasyMock and jMock, both of which are based on java.lang.reflect.Proxy, which requires an interface to be implemented. It's also possible to create proxies for concrete classes with CGLIB-based extensions to these tools. Each of these tools have a rich API for expressing expectations that are verified when the methods on mock classes are called. It's not uncommon to see JUnit tests with most or all checks written as EasyMock/jMock expectations, instead of with JUnit own assertion methods. In that sense, such tools compete with the base testing framework.

Issues with mock objects

The current solutions for achieving isolation with mock objects impose certain design restrictions on the code under test. JMockit was created as an alternative with no such restrictions, by leveraging the facilities in the new java.lang.instrument package (however, code under test as well as test code can still be written in pre-Java 5 syntax).

In any case, application classes for which a mock object is needed must not be final, and any methods to be replaced with mock implementations must be public non-final instance methods. Additionally, the creation of dependent objects must be controlled, so that a proxy object (the mock) can be passed to client classes (see dependency injection). That is, proxied classes can't simply be created by "new" in client code.

To sum up, these are the design restrictions one has to live with when using a conventional approach to mocking:

Having to implement an interface or not being able to make the class final. Also, no method for which a mock implementation might be needed can be final. In Java, making classes and methods final is optional, but can be a great way to capture certain design decisions; additionally, it allows static analysis tools (such as Checkstyle, PMD, or your Java IDE) to provide useful warnings about the code (about, for example, a final method which declares to throw a specific checked exception, but doesn't actually throw it; such warning could not be given for a non-final method, since an override could throw the exception). No static methods for which a mock implementation might be needed can be used. In practice, this means the use of a "static facade", otherwise a convenient solution to provide utility methods or to simplify complex subsystems, is disallowed. All "mockable" methods must be public for mock classes that implement an interface, or at least protected for mock subclasses. Usually private methods don't need to be mocked, but in certain tests it may be the easiest solution in getting the test done. Having to provide the code under test with mock instances of classes they depend on. Although this can be cleanly encapsulated with the use of singletons or registries of service objects (the "Service Locator" pattern), it rules out the simplest possible way to get a service object: by directly instantiating it with the "new" operator.

An example

Say you have a domain layer class (a "service" class) with a business operation having two kinds of dependencies: a) it searches a database using a static facade to the persistence subsystem; and b) it calls a business method in another service class.

   public final class ServiceA
   {
      public void doBusinessOperationXyz(EntityX data) throws InvalidItemStatus
      {
(1)      List items = Database.find(
            "select item from EntityY item where item.someProperty=?",
            data.getSomeProperty());

(2)      BigDecimal total = new ServiceB().computeTotal(items);

         data.setTotal(total);
(3)      Database.save(data);
      }
   }

   public final class ServiceB
   {
      public BigDecimal computeTotal(List items) throws InvalidItemStatus
      {
         BigDecimal total = new BigDecimal(0);
         // compute total while iterating items, or throws InvalidItemStatus exception
         return total;
      }
   }

The Database class contains only static methods and a private constructor; the "find" and "save" methods should be obvious, so we won't list them here (assume they are implemented on top of an ORM API, such as Hibernate).

So, how can we unit test the "doBusinessOperationXyz" method without making any changes to the existing application code? The following JUnit test case does so by mocking out the three dependencies of the method under test, that is, the method calls at (1), (2), and (3).

   public class ServiceATest extends TestCase
   {
      private boolean serviceMethodCalled;

      public static class MockDatabase
      {
         static int findMethodCallCount;
         static int saveMethodCallCount;

         public static void save(Object o)
         {
            assertNotNull(o);
            saveMethodCallCount++;
         }

         public static List find(String ql, Object arg1)
         {
            assertNotNull(ql);
            assertNotNull(arg1);
            findMethodCallCount++;
            return Collections.EMPTY_LIST;
         }
      }

      protected void setUp() throws Exception
      {
         super.setUp();
         MockDatabase.findMethodCallCount = 0;
         MockDatabase.saveMethodCallCount = 0;
         Mockit.redefineMethods(Database.class, MockDatabase.class);
      }

      public void testDoBusinessOperationXyz() throws Exception
      {
         final BigDecimal total = new BigDecimal("125.40");

         Mockit.redefineMethods(ServiceB.class, new Object() {
            public BigDecimal computeTotal(List items)
            {
               assertNotNull(items);
               serviceMethodCalled = true;
               return total;
            }
         });

         EntityX data = new EntityX(5, "abc", "5453-1");
         new ServiceA().doBusinessOperationXyz(data);

         assertEquals(total, data.getTotal());
         assertTrue(serviceMethodCalled);
         assertEquals(1, MockDatabase.findMethodCallCount);
         assertEquals(1, MockDatabase.saveMethodCallCount);
      }

      public void testDoBusinessOperationXyzWithInvalidItemStatus()
      {
         Mockit.redefineMethods(ServiceB.class, new Object() {
            public BigDecimal computeTotal(List items) throws InvalidItemStatus
            {
               throw new InvalidItemStatus();
            }
         });

         EntityX data = new EntityX(5, "abc", "5453-1");

         try {
            new ServiceA().doBusinessOperationXyz(data);
            fail(InvalidItemStatus.class + " was expected");
         }
         catch (InvalidItemStatus ignore) {
            // ok, test passed
            assertNull(data.getTotal());
            assertEquals(1, MockDatabase.findMethodCallCount);
            assertEquals(0, MockDatabase.saveMethodCallCount);
         }
      }
   }

Note this test class extends JUnit's TestCase, and so needs to be run with a JUnit test runner. You also need to pass "-javaagent:jmockit.jar" in the command line to the "java" command, in order to let the JVM provide instrumentation access to JMockit. If jmockit.jar is not in the current working directory, insert the appropriate path after the colon.

Conclusion

With JMockit, any design can be tested in isolation without restricting the developer's freedom. Design decisions which have a negative effect on testability when using only traditional mock objects are inconsequential when using this new approach. In effect, testability becomes much less of an issue in application design, allowing developers to avoid complexities such as explicit interfaces, factories, dependency injection and so on, when they aren't justified by actual system requirements.

Another difference between JMockit and previous tools for mocking is that its API is very small and simple (5 static methods). Instead of using a rich set of tool-specific methods to define expectations, the test writer can continue using well known JUnit assertions.

JMockit requires tests to be run under a Java SE 5 VM. Note however that code under test and the test code itself can be written and compiled in any version of the language.


You are viewing a mobilized version of this site...
View original page here

Mobilized by Mowser Mowser