Mockito Test Cases and Tests where exceptions are expected

To write quality code it’s necesary to test it, as everybody knows and hopefully practices.

How do you avoid super complex, fragile Unit tests that span hundreds of lines of code?

The answer is you don’t use the real boiler-plate objects, you use test double objects (mock objects).

Let’s use Mockito to demonstrate how this excellent framework solves this problem elegantly.

Mockito is a Java framework that allows the creation of mock objects in unit tests for the purpose of test-driven development (TDD).

First some important terminology notes: Spies vs Mocks.

You can create spies of real objects. Meaning you do have an instance of a real boilerplate object, and when you use the spy the real methods are called (unless a method is stubbed).
Or you can create mocks – where no real object’s methods are called, all of the methods are mocked.

  • @Mock annotation is used for mock creation.
  • @Spy annotation is used to create a spy instance.
  • @InjectMocks is used to instantiate the tested object automatically and the framework will inject all the @Mock or @Spy annotated field dependencies into it.
  • To process annotations, we use the framework-provided MockitoJUnitRunner:
@RunWith(MockitoJUnitRunner.class)

Now let’s review the sample code and its key points.

There are two unit tests:

  1. csvColumnCountShouldBe3() and
  2. testParserWithMockDao().

The csvColumnCountShouldBe3() method simply demonstrates how to write a unit test that verifies that an Exception is thrown when a CSV line has wrong number of fields. If method InputValidator.checkColumnCountConsistent doesn’t throw the MyException – you’ll get a failed test:

java.lang.AssertionError: Expected exception: java1.MyException
at org.junit.internal.runners.statements.ExpectException.evaluate(ExpectException.java:32)

If it does throw the Exception (as per design) – the test will pass.

The other method testParserWithMockDao() demonstrates how mock objects are used and verified.

  • Line 32 : Mock creation of Dao via single annotation
  • Line 38: Mark a field on which Dao mock injection should be performed
  • Lines 46-52 define what mocks should do when their methods are called
  • Lines 55-56 run the test code
  • Line 59 validates that the mock objects were indeed used and called the right number of times

Full code is below.

package java1;


import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.*;


@RunWith(MockitoJUnitRunner.class)
public class Test2 {

	// -------------------- expected: MyException --------------------
	// indicating that this Unit test has to throw MyException to pass
	@Test(expected = MyException.class)
	public void csvColumnCountShouldBe3() throws MyException {
		// 3 columns - OK
		InputValidator.checkColumnCountConsistent(3, "col1, col2, col3");
		//2 columns - should throw exception
		InputValidator.checkColumnCountConsistent(3, "col1, col2 - thisRow is missing Col3");
	}

	// -------------------- Mockito test --------------------
	
	// Mock creation of Dao via single annotation
	@Mock
	Dao daoMock;
	

	// Mark a field on which Dao mock injection should be performed
	// @InjectMocks allows shorthand mock and spy injection. Minimizes repetitive coding.
	@InjectMocks
	Parser parser;
	
	
	@Test
	public void testParserWithMockDao() {

		// define Dao mock behavior: 
		// when getting trade #111 - return new Trade with id 111
		Trade t = new Trade(111);
		t.assetClass = "FX";
		when(daoMock.getTrade(111)).thenReturn(t);
		// when getting trade #111 - return new Trade with id 222
		t = new Trade(222);
		t.assetClass = "IR";
		when(daoMock.getTrade(222)).thenReturn(t);
		
		// parser internally calls dao.getTrade internally
		parser.setAssetClass("FX-Foreign exhange", 111);
		parser.setAssetClass("IR-Interest rates", 222);
		
		// verify that a getTrade call in daoMock happened twice
        verify(daoMock, times(2)).getTrade(anyInt());
		 
	}
	

	// Convenience classes for this demo part 1
	private static class MyException extends Exception {
		public MyException(String s) {
			super(s);
		}		
	}

	private static class InputValidator {
		public static void checkColumnCountConsistent(int i, String csv) throws MyException {
			String [] parts = csv.split(",");
			System.out.println(csv + " => Check Columns: " + parts.length);
			if (i != parts.length) {
				MyException e = new MyException("Wrong # of columns: " + parts.length);
				System.out.println(csv + " => Will throw " + e);
				throw e;
			}
		}
	}

	// Convenience classes for this demo part 2
	private static class Parser {

		private Dao dao;
			
		public Parser(Dao dao) {
			this.dao = dao;
		}
		
		public String setAssetClass(String ac, int tradeId) {
			Trade t = dao.getTrade(tradeId);
			t.assetClass = t.parseAndSetAssetClass(ac);
			System.out.println("Parser: parsed asset class as " + t.assetClass);
			return t.assetClass;
		}
		
		
	}
	
	private static class Trade {
		
		public int id;
		public String assetClass = "Unknown";
		
		public Trade(int id) {
			this.id = id;
		}

		public String parseAndSetAssetClass(String ac) {
			System.out.println("Parsing: " + ac);
			// some complex logic...
			this.assetClass = ac.substring(0,2);
			return assetClass;
		}
		
		@Override
		public String toString() {
			return String.format("Trade #%d (%s)", id, assetClass);
		}

	}
	
	private static class Dao {
		
		
		public Dao buildDao(String dbName) throws MyException {
			
			if (dbName == null) {
				throw new MyException("Bad db name - you passed null!");
			}
			Dao d = new Dao();
			return d;
		}
		
		public Trade getTrade(int id) {
			// ... select a row from DB ...
			Trade t = new Trade(id);
			System.out.println("Retrieved trade " + t);
			return t;
		}
		
		public void saveTrade(Trade t) {
			// ... store a row in a DB ...
			System.out.println("Saved trade " + t);
		}

	}
	
	

}