Do you ever come across a situation where your Unit test ends up testing more than what a unit is supposed to be? Does your unit test end up looking more like an Integration test? Does it only run within a container? Does it depend on external things like records in a table, entries in ldap or data in some files? If your answer to any of the above questions is yes, then jMock could be the solution to your unit testing problem.
jMock is a library for testing Java code with mock objects. It sits on top of your existing unit testing framework (eg.Junit). It allows you to mock out interactions between objects hence preventing external dependencies.
You can write a jMock unit test using 6 simple steps:
- Initialize input:- This is where you initialize all static data that will be used for the current unit test.
- Initialize mocks:- Mock out your external dependencies here.
- Setup expectations:- After you create the mock objects, you need to specify what interactions you are expecting to have with them. If the interactions dont happen as you expected, the test fails.
- Initialize subject:- Subject is the object that you are trying to unit test. You will create an instance of the object and initialize it here.
- Invoke Subject:- Call the method to be tested with static input.
- Assert:- Make sure output is as expected and all expectations set on the mocks have been satisfied.
Here is an example of how to go about using jMock:-
AccountService exposes a “getAccountInformation” method which we want to unti test. This method first validates the input, authenticates credentials with ldap and then fetches the account information from the database. It uses a DB accessor and ldap authenticator to do so. First lets look at the AccountService:
package com.amarphadke.jmockdemo.service; import com.amarphadke.jmockdemo.accessor.Authenticator; import com.amarphadke.jmockdemo.accessor.DataAccessor; import com.amarphadke.jmockdemo.domain.Account; import com.amarphadke.jmockdemo.exception.DomainException; import com.amarphadke.jmockdemo.exception.ValidationException; import com.amarphadke.jmockdemo.transport.AccountInformation; import com.amarphadke.jmockdemo.transport.AccountInformationRequest; public class AccountService { private DataAccessor dataAccessor; private Authenticator authenticator; public AccountService(DataAccessor dataAccessor, Authenticator authenticator) { super(); this.dataAccessor = dataAccessor; this.authenticator = authenticator; } public AccountInformation getAccountInformation( AccountInformationRequest request) throws ValidationException, DomainException { validateInput(request); Account account = dataAccessor.fetchAccount(request.getAccountId()); AccountInformation accountInformation = convertDomainObjectToTransportObject(account); return accountInformation; } private AccountInformation convertDomainObjectToTransportObject(Account account) { AccountInformation accountInformation = new AccountInformation(account .getAccountId(), account.getAccountType(), account .getLastAccessedDate(), account.getFirstName(), account .getLastName(), account.getBalance()); return accountInformation; } private void validateInput(AccountInformationRequest request) throws ValidationException { if (request.getAccountId() <= 0) { throw new ValidationException(1, "Invalid Account Id"); } if (request.getUsername() == null) { throw new ValidationException(2, "Invalid Username"); } if (request.getPassword() == null) { throw new ValidationException(3, "Invalid Password"); } if (!authenticator.isValidCredential(request.getAccountId(), request .getUsername(), request.getPassword())) { throw new ValidationException(4, "Invalid Credentails for Account"); } } }
Now, lets take a look at the unit test:
package com.amarphadke.jmockdemo.service; import java.util.Calendar; import java.util.Date; import junit.framework.TestCase; import org.jmock.Expectations; import org.jmock.Mockery; import com.amarphadke.jmockdemo.accessor.Authenticator; import com.amarphadke.jmockdemo.accessor.DataAccessor; import com.amarphadke.jmockdemo.domain.Account; import com.amarphadke.jmockdemo.exception.DomainException; import com.amarphadke.jmockdemo.exception.ValidationException; import com.amarphadke.jmockdemo.service.AccountService; import com.amarphadke.jmockdemo.transport.AccountInformation; import com.amarphadke.jmockdemo.transport.AccountInformationRequest; public class AccountServiceTest extends TestCase { private Mockery context = new Mockery(); /* * Test for AccountService.getAccountInformation(AccountInformationRequest) * with valid input. */ public void testGetAccountInformationWithValidInput() throws ValidationException, DomainException { // Step 1: Initialize input final long accountId = 123456789; final String username = "jmockDemoUser"; final String password = "encryptedPassword"; String accountType = "Checking"; Double balance = new Double(100000000.99); String firstName = "Richie"; String lastName = "Rich"; Date lastAccessedDate = Calendar.getInstance().getTime(); final AccountInformationRequest request = new AccountInformationRequest( accountId, username, password); final Account account = new Account(accountId, accountType, lastAccessedDate, firstName, lastName, balance, null, null); final AccountInformation accountInformation = new AccountInformation( accountId, accountType, lastAccessedDate, firstName, lastName, balance); // Step 2: Initialize mocks final DataAccessor dataAccessor = context.mock(DataAccessor.class); final Authenticator authenticator = context.mock(Authenticator.class); // Step 3: Setup Expectations context.checking(new Expectations() { { oneOf(authenticator).isValidCredential(accountId, username, password); will(returnValue(true)); oneOf(dataAccessor).fetchAccount(accountId); will(returnValue(account)); } }); // Step 4: Initialize Subject AccountService accountService = new AccountService(dataAccessor, authenticator); // Step 5: Invoke Subject AccountInformation actualAccountInformation = accountService .getAccountInformation(request); // Step 6: Assert assertEquals(accountInformation, actualAccountInformation); context.assertIsSatisfied(); } }