04
Apr
10

Using JMock2 to Unit Test Java Code


This page describes the usage of JMock2 framework with JUnit in order to unit test Java code.

Background

No man is an island. By now most everyone has heard this phrase that means that “Human beings do not thrive when isolated from others.” You can say the same for Java classes. Even in the most well designed systems there are always some sort of dependency on other classes. This makes unit testing difficult. This could be the reason developers are turned off from unit testing (not counting laziness).

The JMock2 Framework allows you to create mock implementations. These mock implementations are coded to to behave in a predefined way by the unit tester, then they are injected into Model classes. The model classes are tricked into thinking that they are dealing with actual implementation classes. This allows the developer to setup specific data scenarios that allow the code to be tested.

To conduct effective unit tests you need to make sure that the data does not change. This means you can’t use the database. This is an absolute requirement for 2 reasons.

  1. The data in the database is not static. Databases are shared resource. Someone can always come and yank the data underneath your test case.
  2. The database is slow. Unit tests should be run often and quickly. A slow running test case will make the developer not want to run the test case.

Following MVC Best Practices

An important aspect of writing test friendly and reliable code is to follow MVC best practices. This means that your View logic must be separated from your model and Controller. In addition the model component should be further broken up into parts. One part has the business logic (Model) and the other has the data access logic. Classes that interact with the database, web services, or File systems are to be called DataManagers or DAO’s.

Dont Test your Data Access Layer

In the grand scheme of things the job of DataManagers or DAO’s are pretty simple. These classes convert data back and fourth between the bean representation and the physical representation (network storage, database, or file system). Once you set them up they will continue to perform that task. Your testing effort should not focus on testing your Data Access Layer.. Let you database, or operating system vendor vendor be responsible for this.

Model Layer

The model layer should contain the business logic for your application. It should not make any assumptions on who is invoking calls on it. For example there must be no reference to the Servlet API in this layer. The model layer should communicate to the Data Access Layer thru interfaces. This is the concept of programming by interfaces.

Testing The Model Layer with JMock2

Once you program by interface you can swap out your data managers with Mock implementations and your model will think it is dealing with the real data manager. Doing things this way tricks your Model into thinking the data is coming from the database. This allow you to setup data scenarios to test specific conditions in your model. You can using code coverage analysis tools like eclEMMA in eclipse to ensure you have reached 100% code coverage in your test cases.

Requirements

  • Basic Understanding of JUnit
  • Basic understanding of Maven (see maven tutorial on the right-nav)
  • Understanding of Programming by Interface

Example Code

The following example will show a very basic usage scenario of Jmock2 with Junit.

First step is to create the project using a maven archetype. Open up the command prompt and navigate to an empty directory.

mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart

groupId: com.test
artifactId: jmockTest

Answer the rest of the questions with defaults “Just hit the enter key”

Modify the pom.xml

Modify the file to look like this…

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.test</groupId>
  <artifactId>jmockTest</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>jmockTest</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  <dependency>
    <groupId>org.jmock</groupId>
    <artifactId>jmock</artifactId>
    <version>2.5.1</version>
    <scope>test</scope>
</dependency>
</dependencies>

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.0.2</version>
                <configuration>
                    <source>1.5</source>
                    <target>1.5</target>
                </configuration>
            </plugin>
        </plugins>
</build>
</project>

Save and exit.

If you are using eclipse then you may want to regenerate your project before importing it into eclipse.
do this by typing:
mvn eclipse:clean eclipse:eclipse

src/main/java/com/test/Account.java

package com.test;

import java.math.BigDecimal;

/**
 * Simple POJO bean that represents an account.
 */
public class Account {
	public String getAccountNumber() {
		return accountNumber;
	}
	public void setAccountNumber(String accountNumber) {
		this.accountNumber = accountNumber;
	}
	public BigDecimal getBalance() {
		return balance;
	}
	public void setBalance(BigDecimal balance) {
		this.balance = balance;
	}
	private String accountNumber;
	private BigDecimal balance;
}

src/main/java/com/test/AccountDataManager.java

package com.test;

public interface AccountDataManager {
	public void saveAccount(Account accountA);
	public Account getAccountById(String accountAId);
}

src/main/java/com/test/AccountDataManagerImpl.java

package com.test;

public class AccountDataManagerImpl implements AccountDataManager {
	/**
	 * This code is simple no need to test Vendor JDBC implementation...
	 */
	public void saveAccount(Account accountA) {
		// some JDBC code to save account to database
		// goes here.
	}

	/**
	 * This code is simple no need to test Vendor JDBC implementation...
	 */
	public Account getAccountById(String accountAId) {
		// some JDBC code to get account from database
		// goes here.
		return null;
	}
}

src/main/java/com/test/AccountTransferModel.java

package com.test;

import java.math.BigDecimal;

public interface AccountTransferModel {

	/**
	 * Transfer money from account a to account b. This should be executed from
	 * within a transaction. (See spring framework)
	 *
	 * @param a
	 * @param b
	 */
	public abstract void transferMoney(String accountAId, String accountBId,
			BigDecimal amount);

}

src/main/java/com/test/AccountTransferModelImpl.java

package com.test;

import java.math.BigDecimal;

/**
 * This class is where the business logic is implemented.
 */
public class AccountTransferModelImpl implements AccountTransferModel {
	/**
	 * Since we are using an interface here we will use JMock2 to
	 * implement the below interface with a fake stub class.
	 */
	private AccountDataManager accountDataManager;

	public void setAccountDataManager(AccountDataManager accountDataManager) {
		this.accountDataManager = accountDataManager;
	}

	/**
	 * This is the method we are testing...
	 */
	public void transferMoney(String accountAId, String accountBId, BigDecimal amount) {
		Account accountA = accountDataManager.getAccountById(accountAId);
		Account accountB = accountDataManager.getAccountById(accountBId);

		if(accountA==null) {
			throw new IllegalArgumentException("account does not exist.");
		}
		if(accountB==null) {
			throw new IllegalArgumentException("account does not exist.");
		}

		if(accountA.getBalance().compareTo(amount) < 0 ) {
			throw new IllegalArgumentException("account A does not have enough balance.");
		}
		accountA.setBalance(accountA.getBalance().subtract(amount));
		accountB.setBalance(accountB.getBalance().add(amount));

		accountDataManager.saveAccount(accountA);
		accountDataManager.saveAccount(accountB);

	}
}

Finally we write our test case.

src/test/java/com/test/AppTest.java

package com.test;

import java.math.BigDecimal;

import junit.framework.TestCase;

import org.jmock.Expectations;
import org.jmock.Mockery;

/**
 * Test to check Money Transfers Between Accounts
 */
public class AppTest extends TestCase {

	public void testTransfersMoneyBetweenAccounts() {
		Mockery context = new Mockery();
		final AccountDataManager accountDataManager = context.mock(AccountDataManager.class);
		AccountTransferModelImpl accountTransferModel = new AccountTransferModelImpl();
		accountTransferModel.setAccountDataManager(accountDataManager);

		// setup the data
		final Account accountA = new Account();
		accountA.setAccountNumber("abc123");
		accountA.setBalance(BigDecimal.valueOf(100));

		final Account accountB = new Account();
		accountB.setAccountNumber("xyz123");
		accountB.setBalance(BigDecimal.valueOf(100));

		// set the expectations
		// if this next section looks odd then go visit the jMock2 site
		// for documentation on how to specify expectations
	    context.checking(new Expectations() {{
	        allowing (accountDataManager).getAccountById("abc123");
	        will(returnValue(accountA));
	        allowing (accountDataManager).getAccountById("xyz123");
	        will(returnValue(accountB));

	        oneOf(accountDataManager).saveAccount(accountA);
	        oneOf(accountDataManager).saveAccount(accountB);
	    }});
		// execute the code

	    accountTransferModel.transferMoney("abc123", "xyz123", new BigDecimal(100));

	    // make the assertions
	    assertEquals(accountA.getBalance(),BigDecimal.valueOf(0));
	    assertEquals(accountB.getBalance(),BigDecimal.valueOf(200));
	}
}

Run the code

Run the code by executing it thru eclipse.

Or type mvn test on the command line.

System should return success condition

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.test.AppTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.061 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Analyse the code

Just Set a couple of breakpoints and trace thru the code using your favorite debugging tool.

References

Thats all for now.

Advertisements

4 Responses to “Using JMock2 to Unit Test Java Code”


  1. April 5, 2010 at 8:10 am

    Thanks for referencing jMock. A couple of things you might want to consider about the example…

    If you just want to match an argument with equals(), you don’t need to use the with() clause, which is really for pushing more specific matchers through the Java type system.

    Our convention is to use allowing() (instead of oneOf()) for queries–calls that don’t change the environment outside the tested object. It’s the same underlying mechanism but we think it makes the intention of the test clearer.

    In this case, I would check that accountA and accountB are actually saved, it’s part of the core functionality of the tested method. Again, you can just call
    oneOf(accountDataManager).saveAccount(accountA);
    since you already have the account objects in your hand.

    One final point, you might want to name the test after the feature it exercises, for example: testTransfersMoneyBetweenAccounts()
    I find it helps me be clear about what I want to achieve.

    • April 5, 2010 at 5:42 pm

      Thanks for your comments and for taking the time to review my tutorial. I have integrated those changes. If you have any more suggestions please let me know. It would be great if you could put a link on your site pointing to this tutorial. If you need any help with documentation or have any ideas for new tutorials that I can write please let me know.

  2. April 16, 2013 at 8:30 am

    Nice post but the thing that I am missing here is the verification of the jMock

    // verify
    context.assertIsSatisfied();


Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Join 77 other followers

April 2010
S M T W T F S
« Mar   May »
 123
45678910
11121314151617
18192021222324
252627282930  

Blog Stats

  • 830,825 hits

%d bloggers like this: