- Use of static methods in the service layer
- No dependency injection or service locators/factories to speak of (dependencies are typically instantiated with "new", or are static method calls)
- Little use of interfaces
However, EasyMock still cannot help us with the first two issues: static methods and lack of dependency injection. So what is a poor Java developer to do? This is where my most recent favorite unit testing tool comes in: JMockit. It can handle all of the above problems and even more, such as mocking final classes and final methods, mocking JDK classes, and more. It is probably the most powerful Java mocking framework currently available. The other contender is PowerMock, but there are several reasons why I prefer JMockit which I will go over later. Another option would be to use Groovy to test your Java code, using the MockFor and StubFor classes, but for the sake of this discussion let's stick to the Java language.
About the only thing that I don't like about JMockit is the name. It's too easily confused with JMock, a less powerful mocking framework that is somewhat similar to EasyMock. Almost every time I've told someone about JMockit, they think I'm talking about JMock and much confusion ensues. Perhaps JMockit should consider a name change...maybe SuperMock? MegaMock? Hmmm...
Anyway, back to my main point. JMockit has made it possible to test any & all of our legacy code without changing the code to fit someone's idea of "testable" code. Here's a quote from the JMockit site that I like a lot on this point:
The set of limitations listed above, which are found in conventional mocking tools, has come to be associated with the idea of "untestable code". Often, we see the restrictions resulting from those limitations considered as inevitable, or even as something that could be beneficial. The JMockit toolkit, which breaks away from these limitations and restrictions, shows that in fact there is no such thing as truly untestable code. There is, of course, code that is harder to test because it is too complicated and convoluted, lacks cohesion, and so on and so forth.
Therefore, by eliminating the technical limitations traditionally involved in the isolation of an unit from its dependencies, we get the benefit that no artificial design restrictions must be imposed on production code for the sake of unit testing. Additionally, it becomes possible to write unit tests for legacy code, without the need for any prior adaptation or refactoring. In short, with a less restrictive mock testing tool the testability of production code becomes much less of an issue, and developers get more freedom in using Java language features, as well as more OO design choices.Here is an example to demonstrate some basic JMockit features. Say we have a Bookstore class, which is the class we want to test (also known as a "system under test", or SUT). It has a dependency on a static method in a BookstoreService class, which is sometimes called a "collaborator class". Let's take a look at the Bookstore class:
package com.olsonzoo.example.legacy; /** * Legacy Code example, to demonstrate JMockit usage. * * @author Jeff Olson (jeff@olsonzoo.com) */ public class Bookstore { public String getBookTitle(String isbn) { return BookstoreService.getBookTitle(isbn); } }
I could show you the implementation of BookstoreService, but in fact we don't really care about that, because to test Bookstore we want to mock out the BookstoreService.getBookTitle() method. This is because we are assuming, for the sake of example, that this method is actually doing something expensive like contacting a remote web service to get the book title. Since this is a static method, though, we need JMockit to come to our rescue. Here is how we would write a couple of tests to do just that:
package com.olsonzoo.example.legacy; import mockit.Expectations; import mockit.Mocked; import org.junit.Test; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.assertThat; /** * Test class for Bookstore. * * @author Jeff Olson (jeff@olsonzoo.com) */ public class BookstoreTest { @Mocked private BookstoreService service; @Test public void testGetBookTitle() throws Exception { final String isbn = "999999999X"; final String expectedTitle = "The Dilbert Principle"; new Expectations() {{ BookstoreService.getBookTitle(isbn); result = expectedTitle; }}; Bookstore store = new Bookstore(); String title = store.getBookTitle(isbn); assertThat(title, equalTo(expectedTitle)); } @Test public void testGetBookTitle_NotFound() throws Exception { final String isbn = "9999999980"; new Expectations() {{ BookstoreService.getBookTitle(isbn); result = null; }}; Bookstore store = new Bookstore(); String title = store.getBookTitle(isbn); assertThat(title, equalTo(null)); } }
Notice a few things here. First, we have declared that BookstoreService is to be mocked out by JMockit by using the @Mocked annotation. Second, we put our expected behavior inside an Expectations block. (The double braces are there because we are instantiating an anonymous class and using an initialization block.) Inside the Expectations block we tell JMockit what call to expect and what the result should be. After the Expectations, we call the Bookstore.getBookTitle() method and then assert that the resulting value is what we expected to get.
And that's it. Because we used an Expectations block, which is strict by default, JMockit automatically does a verification at the end to make sure that the methods expected were actually called (and no more). There is also a NonStrictExpectations alternative which allows you to be more lenient about which methods are called on the mocked classes, but in that case you have to do any verification yourself by using a Verifications() block, similar to the Expectations.
And that is a simple example using JMockit's behavior-based testing support. Another alternative is state-based testing. Behavior-based testing is the approach typically used in EasyMock, and is concerned with testing the details of the interactions between the object under test and the collaborator object. State-based testing, on the other hand, tends to be used when the interactions are not as important and a "stub" object is used in place of the collaborator. Martin Fowler has a great overview of the differences between mocks and stubs if you are interested.
Here is a JMockit example that tests the same method in Bookstore, but this time using state-based testing.
package com.olsonzoo.example.legacy; import com.google.common.collect.Maps; import mockit.Mock; import mockit.MockClass; import org.junit.BeforeClass; import org.junit.Test; import java.util.Map; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; /** * Test class for Bookstore. * * @author Jeff Olson (jeff@olsonzoo.com) */ public class BookstoreStateBasedTest { private static Map<String, String> bookMap = Maps.newHashMapWithExpectedSize(2); @BeforeClass public static void setup() { bookMap.put("0553293354", "Foundation"); bookMap.put("0836220625", "The Far Side Gallery"); } @MockClass(realClass = BookstoreService.class) public static class MockBookstoreService { @Mock public static String getBookTitle(String isbn) { if (bookMap.containsKey(isbn)) { return bookMap.get(isbn); } else { return null; } } } @Test public void testGetBookTitle() throws Exception { final String isbn = "0553293354"; final String expectedTitle = "Foundation"; Bookstore store = new Bookstore(); String title = store.getBookTitle(isbn); assertThat(title, equalTo(expectedTitle)); } @Test public void testGetBookTitle_NotFound() throws Exception { final String isbn = "9999999980"; Bookstore store = new Bookstore(); String title = store.getBookTitle(isbn); assertThat(title, equalTo(null)); } }
Notice that the BookstoreService is replaced by the MockBookstoreService by using the @MockClass annotation. Likewise, the getBookTitle() method is replaced by the mocked version using the @Mock annotation. It's really that simple.
You can also mock out constructors and static initializers by using the special $init and $clinit methods. See the JMockit documentation for more details.
To include JMockit when running your unit tests, all you need to do is make sure you are running your tests under JDK 1.5 or later, and include the following line in your VM arguments (assuming your jmockit.jar is in the lib directory):
-javaagent:lib/jmockit.jar
Finally, what about PowerMock? PowerMock has a lot of the same features as JMockit. However, one thing that I have seen in my limited experience in trying out PowerMock is that is much more low-level. You have to explicitly list out using the @PrepareForTest annotation which classes you want PowerMock to mock out for you. JMockit is much easier to use in that regard.
In conclusion, if you have legacy code that doesn't meet the traditional "testable" criteria, give JMockit a try.