Thursday, May 18, 2017

Parameterized Junit test

I have used the Parameterized Junit test in places where you need to test a method with huge combinations of input. Imagine a truth table logic in place.
Form the truth table -> let's take 2 inputs for simplicity sake.
A
B
Expected
1
1
1
1
0
1
0
1
1
0
0
0

Say if the method is,
Method(param1, param2)
Now if I have to go with the conventional junit test cases, I would have to name my methods like,
  • testMethodForParam1TrueParam2False()
  • testMethodForParam1TrueParam2True()
  • testMethodForParam1FalseParam2True()
  • testMethodForParam1FalseParam2False()
Sure it looks readable now. But consider the same for a 3 X 3 matrix. It becomes ugly.
Wouldn't it better to actually have this truth table in the start of the test case and feed it to just one test?
This is exactly what Parameterized Junit solves.
There are various blogs and sources to refer the syntax of such test.
But when you want to use it in an integration test, you need to keep the following in mind.
  1. As long as your data is static, you have nothing to worry about. Say, your service which contains the method, is initialized in @BeforeClass and in your @Parameterized.Parameters the data you feed is coming from a static method in that service. Then you will get NPE. Because @Parameters is called before @BeforeClass. To tackle this, have your initialization in a simple static block. But then, you need to make the test class final. @BeforeClass will apply to all the inherited classes, static block will not.
    package com.foo;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertNotNull;
    
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.Date;
    import java.util.List;
    import java.util.Optional;
    
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.junit.runners.Parameterized;
    
    
    @RunWith(Parameterized.class)
    public final class ServiceTest {
     private static Service service;
    
     private Integer param1;
     private Integer param2;
     private Integer expectedResult;
     private String caseName;
    
     static {
     setUpClass();
     }
    
     public static void setUpClass() {
     service = (Service) CustomContextLoader.getBean("service");
     }
    
     public ServiceTest(String caseName, Integer param1, Integer param2, Integer expectedResult) {
     this.caseName = caseName;
     this.param1 = param1;
     this.param2 = param2;
     this.expectedResult = expectedResult;
     }
    
     @Parameterized.Parameters
     public static Iterable<Object[]> testData()
     {
     return Arrays.asList(new Object[][]{
     { "case 1 : 1 1 = 1", 1, 1, 1},
     { "case 2 : 1 0 = 1", 1, 0, 1},
     { "case 3 : 0 1 = 1", 0, 1, 1},
     { "case 4 : 0 0 = 0", 0, 0, 0},
     });
    
     }
    
     @Before
     public void setup() {
     System.out.println("@Before");
     }
    
     @Test
     public void testMethod() {
     Integer result = service.orMethod(this.param1, this.param2);
     assertEquals(this.caseName+" failed", this.expectedResult, result);
     }
    
    }
    Remember the order of execution
    Static block
    @Parameter
    @BeforeClass
    @Before
    @Test
    For my case, I had to bootstrap some data (think of inserting some rule and referencing that throughout your test case using an ID. In such case I need to insert the rule and get the ID before I can setup the data) before I can set test data. Hence it was important for the service to be initialized before it hit the @Parameters method. I could not use @BeforeClass because it gets executed after @Parameters. Hence I had to resort to static method.
  2. Add a description for each test data you setup in the @Parameters. and add this to the assertion string. This will help identify which dataset is failing. Because, since this is parameterized test, the test results would be printed something like this:
    com.foo.ServiceTest > testMethod[0] PASSED
    com.foo.ServiceTest > testMethod[1] PASSED
    com.foo.ServiceTest > testMethod[2] FAILED
    java.lang.AssertionError
    com.foo.ServiceTest > testMethod[3] PASSED
    Now if you have setup a caseName with the testData, you will spend less time debugging the issue.
    com.foo.ServiceTest > testMethod[0] PASSED
    com.foo.ServiceTest > testMethod[1] PASSED
    com.foo.ServiceTest > testMethod[2] FAILED
    java.lang.AssertionError : case 3 : 0 1 = 1 failed
    com.foo.ServiceTest > testMethod[3] PASSED
    Also note that the parameterized junit test runs like there are separate @Test methods for all of those data sets. Means it will continue running all the tests even if one fails. It is not fail-fast.

No comments:

Post a Comment