Saturday, September 06, 2008

A Simple Unit Test.

Test Driven Development is something people normally talk about but sometimes we have to do enhacements or work on existing code which does not have Unit Tests. In those situations also the best thing to do before start working or doing enhancements is to write unit tests for the existing code. Over here I would be trying to write few simple test cases on an existing piece of code.
Below is a Math class, used for doing addition, which loads a Calculator from Spring ApplicationContext and does the addition.

package com.vikrant;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Math {

private ClassPathXmlApplicationContext springAppCtx;

Math(){
springAppCtx = new ClassPathXmlApplicationContext("appcontext.xml");
}

int add(int a, int b, int c) {
Calculator calc = (Calculator) springAppCtx.getBean("calculator");
try {
return calc.add(calc.add(a, b),c);
} catch (Exception e) {
throw new CalculationException(e);
}
}
}

Goal is to have a test case which tests the unit called add in class Math, which would test the following,
  1. Spring Application context is called to returns a Calculator Object or throws an exception.
  2. Calculator is called twice to get the actual result which is returned by the method.
  3. If calculator.add method fails then the method under test should throw a Calculation Exception.

So following would be the test case we will write using JUnit 4.1


package com.vikrant;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class MathTest {

@Test
public void addSuccessfully() {
Math m = new Math();
int result = m.add(1, 1, 1);
assertEquals(3, result);
}

@Test(expected=Exception.class)
public void getBeanFailsToLoadCalculator(){
Math m = new Math();
m.add(1, 1, 1);
}

@Test(expected=CalculationException.class)
public void addCallThrowsException(){
Math m = new Math();
m.add(1, 1, 1);
}
}


Things to Note over here are :-
  1. Package of the test case is same as that of the class under test a usual practice followed, benefits are :a) Easy to identify which testcase is for which class.b) Provides access to inaccessible methods like add above.
  2. Names of the test case provide enough information about what the test is for, which removes the need for documentation of test case plus test reports become more readable and meaningful. Another problem with documentation is, its not available in the test reports and hence they are bit difficult to understand if the test names are confusing.
  3. Problems with the above test case are a) First test is not only testing the Math.add method but also everything that is called from inside it, which actually makes the test ambigous, because the expectation is to test the logic in add method and not the Spring Application Context Loading and the Calculator, which itself is a separate class and should be having a separate test. b) How do we make the test number 2 and 3 successfully run c) We don't know how many times the add method is called.
Answer to the problems in Point #3 is use Mock Objects. Benefits are :-
  1. Spring getBean method and calculator can be mocked so that we just test the logic in Math.add method and not other methods which are called from it.
  2. By mocking methods from external libraries like Spring getBean method, dependence on external libraries goes down and hence the tests would still run if spring library fails to load.
  3. Mock objects can be created to throw exceptions so that the real scenario can be simulated in test environment to verify if everything works as expected.
  4. Using mocks causes the tests to execute faster and in much less time as there is no dependence on external libraries/services and systems like DB etc. Time of execution is very important factor because the count of tests would be very high and they all might be required to be run multiple times during the day.
  5. While doing TDD I was using jMock library and found EasyMock to be good as well, but never used it that much. Recently I started using JMockit and found it to be quite easy to learn, plus it helps mock even static methods, which are quite often available in old/existing code.
Following is the same Test case using JMockit mock library.


package com.vikrant;

import static org.junit.Assert.assertEquals;
import mockit.Expectations;
import mockit.Mockit;

import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.BeansException;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MathTest {

private Math math;

@Before
public void before() {
Mockit.redefineMethods(Math.class, new MockMath(), true);
math = new Math();
//Recording phase.
new Expectations() {
{
setField(math, "springAppCtx", new ClassPathXmlApplicationContext());
}
};
}

public static class MockMath {
public MockMath() {
}
}

public static class MockCalculator{
static int addCallCnt;
public MockCalculator(){
addCallCnt = 0;
}
public int add(int a, int b) {
addCallCnt++;
return a + b;
}
}

@Test
public void calculatorIsLoadedAndAddCalled2Times() {
Mockit.redefineMethods(AbstractApplicationContext.class, MyApplicationContext1.class);
Mockit.redefineMethods(Calculator.class, new MockCalculator(), true);

int result = math.add(1, 1, 1);
assertEquals(3, result);
assertEquals(2, MockCalculator.addCallCnt);
}

public static class MyApplicationContext1 {
public Object getBean(String beanId) throws BeansException {
return new Calculator();
}
}

@Test(expected = Exception.class)
public void getBeanFailsToLoadCalculator() {
Mockit.redefineMethods(AbstractApplicationContext.class, MyApplicationContext2.class);
Mockit.redefineMethods(Calculator.class, new MockCalculator(), true);

math.add(1, 1, 1);
}

public static class MyApplicationContext2 {
public Object getBean(String beanId) throws BeansException {
throw new RuntimeException("Forcibly Failed");
}
}

@Test(expected = CalculationException.class)
public void addCallThrowsException() {
Mockit.redefineMethods(AbstractApplicationContext.class, MyApplicationContext1.class);
Mockit.redefineMethods(Calculator.class, new Object() {
public int add(int a, int b) {
throw new RuntimeException("Forcibly Failed");
}
}, true);

math.add(1, 1, 1);
}
}

Note : Some of the above mock related things can be done with lesser code using jMock.

1 comment:

Jyoti Arora said...

U started with ur favourite topic..
Junit and above all MockIt :)
Kool blog.. shd give an insight to all the ppl who are new to writing usnig Junit & JMockIt.