Getting Started with EasyMock for Unit Testing
Installing EasyMock
EasyMock is a library that simplifies the creation of mock objects for specified interfaces or classes, facilitating unit testing through simulation of collaborative modules or domain objects. To use EasyMock in your project, simply add the EasyMock JAR file to your classpath.
Using EasyMock
The process of using EasyMock can be broken down into these key steps:
- Generating mock objects with EasyMock
- Setting expected behaviors and outputs for mock objects
- Switching mock objects to Replay state
- Invoking mock object methods for unit testing
- Verifying mock object behaviors
Generating Mock Objects
EasyMock can dynamically create mock objects based on specified interfaces or classes.
Creating Mock Objects for Interfaces
Consider the ResultSet interface as an example:
public interface java.sql.ResultSet {
public abstract java.lang.String getString(int arg0) throws java.sql.SQLException;
public abstract double getDouble(int arg0) throws java.sql.SQLException;
// Other methods...
}
Creating a real ResultSet object typically involves a complex process: developers often write a DBUtility class to obtain a database connection Connection, then use the Connection to create a Statement. Executing the Statement yields one or more ResultSet objects. This construction process is complex and depends on proper database operation. Issues with the database or database interaction modules can affect unit test results.
We can solve this problem by dynamically creating a mock object for the ResultSet interface using EasyMock:
import static org.easymock.EasyMock.createMock;
ResultSet mockResultSet = createMock(ResultSet.class);
Alternatively:
import org.easymock.EasyMock;
ResultSet mockResultSet = EasyMock.createMock(ResultSet.class);
Creating Mock Objects for Classes
By default, EasyMock only supports creating mock objects for interfaces. To create mock objects for concrete classes, you need to download the EasyMock Class Extension package. When mocking concrete classes, simply replace org.easymock.EasyMock with org.easymock.classextension.EasyMock.
Managing Multiple Mock Objects with IMocksControl
For more complex test cases using multiple mock objects, you can use the mechanism provided by EasyMock to create and manage mock objects.
The createControl method of the EasyMock class creates an IMocksControl object, which can create and manage multiple mock objects.
IMocksControl control = EasyMock.createControl();
java.sql.Connection mockConnection = control.createMock(Connection.class);
java.sql.Statement mockStatement = control.createMock(Statement.class);
java.sql.ResultSet mockResultSet = control.createMock(ResultSet.class);
Setting Expected Behaviors and Outputs
In a complete test, a mock object goes through two states: Record and Replay.
When a mock object is created, it is in Record state. In this state, users can set expected behaviors and outputs for the mock object. These behaviors are recorded and saved in the mock object.
The process of adding mock object behaviors consists of three steps:
- Make specific method calls on the mock object
- Use the
expectLastCallstatic method provided byorg.easymock.EasyMockto get theIExpectationSettersinstance corresponding to the last method call - Use the
IExpectationSettersinstance to set expected outputs (two types):- Return values
- Throw exceptions
Setting Expected Return Values
Setting return values corresponds to the andReturn method of the IExpectationSetters interface.
IExpectationSetters<T> andReturn(T value);
Non-void Return Values
For example, using the ResultSet mock object, if we want the mockResultSet.getString(1) method to return "My return value", we would write:
mockResultSet.getString(1);
EasyMock.expectLastCall().andReturn("My return value");
Or:
EasyMock.expect(mockResultSet.getString(1)).andReturn("My return value");
If you want a method to always return the same value without setting the mock object behavior each time, you can use the default return value method — andStubReturn method.
void andStubReturn(Object value);
For example, suppose we've created mock objects for the Statement and ResultSet interfaces: mockStatement and mockResultSet. During testing, we want the executeQuery method of the mockStatement object to always return mockResultSet. We would write:
mockStatement.executeQuery("SELECT * FROM sales_order_table");
EasyMock.expectLastCall().andStubReturn(mockResultSet);
Or:
EasyMock.expect(mockStatement.executeQuery("SELECT * FROM sales_order_table")).andStubReturn(mockResultSet);
Void Return Values
For methods with a void return type, we don't need to set return values; we only need to set the number of calls (which is optional).
For example, using the close method of the ResultSet interface, if we want this method to be called 3 to 5 times during testing, we would write:
mockResultSet.close();
EasyMock.expectLastCall().times(3, 5); // Newer versions of EasyMock may omit this line
Or:
EasyMock.expect(mockResultSet.close()).times(3, 5);
Setting Expected Exceptions
Setting expected exceptions corresponds to the andThrow method of the IExpectationSetters interface.
IExpectationSetters<T> andThrow(Throwable throwable);
Similarly, setting default exceptions corresponds to the andStubThrow method of the IExpectationSetters interface.
void andStubThrow(Throwable throwable);
For example:
EasyMock.expectLastCall().andThrow(new MyException(new RuntimeException())).anyTimes();
Setting Expected Method Call Counts
- Setting exact call counts: Use the
timesmethod of theIExpectationSettersinterface.IExpectationSetters<T> times(int count);For example, if we want the
getStringmethod ofmockResultSetto be called 3 times during testing, with each call returning "My return value", we would write:mockResultSet.getString(1); expectLastCall().andReturn("My return value").times(3);Or:
EasyMock.expect(mockResultSet.getString(1)).andReturn("My return value").times(3); - Setting inexact call counts:
times(int minTimes, int maxTimes): The method should be called at leastminTimesand at mostmaxTimes.atLeastOnce(): The method should be called at least once.anyTimes(): The method can be called any number of times.
Runtime Return Values
Sometimes, the expected return value of a method isn't fixed but depends on the input parameters. In such cases, you need to use the andAnswer method:
EasyMock.expect(mockService.execute(EasyMock.anyInt())).andAnswer(new IAnswer<Integer>() {
public Integer answer() throws Throwable {
Integer count = (Integer) EasyMock.getCurrentArguments()[0];
return count * 2;
}
});
Note: You can get the input parameters using EasyMock.getCurrentArguments()!
Switching Mock Objects to Replay State
During both the mock object generation and behavior setting phases, the mock object is in Record state, where it records user-defined expected behaviors and outputs.
Before using the mock object for actual testing, you need to switch its state to Replay. In this state, the mock object responds to specific method calls according to the defined expectations.
There are two ways to switch an object to Replay state:
- If the mock object was generated using the
createMockmethod:EasyMock.replay(mockResultSet); - If the mock object was generated using the
createMockmethod of anIMocksControlinterface object:control.replay();This statement switches all mock objects generated through the
control'screateMockmethod to Replay state.
Invoking Mock Object Methods for Unit Testing
This section will be covered in a JUnit + EasyMock example.
Verifying Mock Object Behaviors
After using the mock object for actual testing, you need to verify the number of method calls made to the mock object.
- If the mock object was generated using the
createMockmethod:EasyMock.verify(mockResultSet); - If the mock object was generated using the
createMockmethod of anIMocksControlinterface object:control.verify();Similarly, this statement verifies all mock objects generated through the
control'screateMockmethod.
Reusing Mock Objects
To avoid generating too many mock objects, EasyMock allows you to reuse existing ones.
You can use the reset method to reinitialize a mock object. After reinitialization, the mock object is set to Record state.
- If the mock object was generated using the
createMockmethod:EasyMock.reset(mockResultSet); - If the mock object was generated using the
createMockmethod of anIMocksControlinterface object:control.reset();Similarly, this statement reinitializes all mock objects generated through the
control'screateMockmethod.
Using Parameter Matchers in EasyMock
When using mock objects for actual testing, EasyMock matches expected method calls based on method name and parameters. By default, EasyMock uses the equals() method for parameter comparison, which can cause issues. Therefore, EasyMock provides various parameter matching approaches.
For example, with the mockStatement object created earlier:
mockStatement.executeQuery("SELECT * FROM sales_order_table");
EasyMock.expectLastCall().andStubReturn(mockResultSet);
In actual usage, you might encounter case sensitivity issues with SQL keywords (since SQL is case-insensitive), such as writing "SELECT" as "select". In this case, the default equals matcher used by EasyMock would consider the parameters not matching, and the expected method on the mock object wouldn't be called.
Predefined Parameter Matchers
anyObject(): Matches any input value.aryEq(X value): Matches usingArrays.equals(), suitable for array objects.isNull(): Matches when the input value is null.notNull(): Matches when the input value is not null.same(X value): Matches when the input value is the same object as the expected value.lt(X value), leq(X value), geq(X value), gt(X value): Matches when the input value is less than, less than or equal to, greater than or equal to, or greater than the expected value, respectively. Suitable for numeric types.startsWith(String prefix), contains(String substring), endsWith(String suffix): Matches when the input value starts with, contains, or ends with the expected value, respectively. Suitable for String types.matches(String regex): Matches when the input value matches the regular expression. Suitable for String types.
For example, if you don't care about the specific SQL statement executed by mockStatement and want all input strings to match this method call:
EasyMock.expect(mockStatement.executeQuery(anyObject())).andStubReturn(mockResultSet);
Custom Parameter Matchers
When predefined parameter matchers don't meet complex requirements, you can define your own. For example, in the previous case, we might want a matcher that is case-insensitive for SQL keywords. Using anyObject isn't ideal, so we can create a custom matcher called SQLCaseInsensitiveEquals.
To create a custom parameter matcher:
- Implement the
org.easymock.IArgumentMatcherinterface:- The
matches(Object actual)method should implement the matching logic between the input and expected values. - The
appendTo(StringBuffer buffer)method can add information to display when matching fails.
- The
- Use a static method to wrap the class implementing the interface.
package org.easymock.demo.matcher;
import static org.easymock.EasyMock.reportMatcher;
import org.easymock.IArgumentMatcher;
public class SQLCaseInsensitiveEquals implements IArgumentMatcher {
private String expectedSQL = null;
public SQLCaseInsensitiveEquals(String expectedSQL) {
this.expectedSQL = expectedSQL;
}
// Information to display when matching fails
public void appendTo(StringBuffer buffer) {
buffer.append("SQLCaseInsensitiveEquals(\"" + expectedSQL + "\")");
}
// Matching logic between input and expected values
public boolean matches(Object actualSQL) {
if (actualSQL == null && expectedSQL == null) return true;
else if (actualSQL instanceof String) return expectedSQL.equalsIgnoreCase((String) actualSQL);
else return false;
}
// Static method for the custom SQLCaseInsensitiveEquals matcher
public static String sqlCaseInsensitiveEquals(String in) {
reportMatcher(new SQLCaseInsensitiveEquals(in));
return in;
}
}
Using the custom sqlCaseInsensitiveEquals matcher:
EasyMock.expect(mockStatement.executeQuery(sqlCaseInsensitiveEquals("SELECT * FROM sales_order_table"))).andStubReturn(mockResultSet);
Special Mock Object Types
The mock objects created so far are of the default EasyMock type, which is insensitive to the order of expected method calls and throws AssertionError for unexpected method calls. Besides this default type, EasyMock provides special mock types to support different needs.
Strict Mock Objects
Strict mock objects are sensitive to the order of method calls. They can be created as follows:
- Using
EasyMock.createStrictMock():ResultSet strictMockResultSet = EasyMock.createStrictMock(ResultSet.class); - Similarly, you can use an
IMocksControlinstance to create a strict mock object:IMocksControl control = EasyMock.createStrictControl(); ResultSet strictMockResultSet = control.createMock(ResultSet.class);
Nice Mock Objects
Nice mock objects return default "invalid" values like 0, null, or false. They can be created as follows:
- Using
EasyMock.createNiceMock():ResultSet niceMockResultSet = EasyMock.createNiceMock(ResultSet.class); - Similarly, you can use an
IMocksControlinstance to create a nice mock object:IMocksControl control = EasyMock.createNiceControl(); ResultSet niceMockResultSet = control.createMock(ResultSet.class);