Unit Testing with JUnit – Part 2

Unit Testing with JUnit – Part 2

5 Comments

In the first part of the series on unit testing with JUnit, we looked at creating unit tests both using Maven and IntelliJ. In this post, we will look at some core unit testing concepts and apply those using JUnit constructs. We will learn about assertions, JUnit 4 annotations, and test suites.

JUnit Assertions

Assertions, or simply asserts provide programmers a way to validate the intended behavior of code. For example, through an assertion you can check whether a method returns the expected value for a given set of parameters or a method correctly sets up some instance or class variables. When you run the test, the assertion executes. If the method under test behaves exactly as you specified in the assertion, your test passes. Otherwise, an AssertionError is thrown.

JUnit provides support for assertions through a set of assert methods in the org.junit.Assert class. Before we start using them, let’s have a quick overview of the Arrange, Act, Assert (AAA) pattern. This pattern is the recommended way to write unit test methods where you divide a method into three sections, each with a specific purpose:

  • Arrange: Initialize objects and set up input data for the method under test.
  • Act: Invoke the method under test passing the arranged parameters.
  • Assert: Verify that the method under test behaves as expected. This is where you write an assertion method.

Here is a Java class we will be writing some JUnit unit tests to test.

EmployeeEmail.java

package guru.springframework.unittest.asserts;

import java.util.HashMap;
import java.util.Map;
import  java.util.regex.*;

public class EmployeeEmail {

    Map<String, String> hashMap = new HashMap<String, String>();

    public  void addEmployeeEmailId(String key, String value){
        if(isValidEmailId(value)) {
            hashMap.put(key, value);
        }
    }
    public String getEmployeeEmailId(Object key){
        if (!(key instanceof String)) {
            throw new IllegalArgumentException("Object not type of String");
        }
        return hashMap.get(key);
    }
    public boolean isValidEmailId(String email){
        String regex = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$";
        Pattern pattern = Pattern.compile(regex);
        Matcher m = pattern.matcher(email);
        return m.matches();
    }
}

In the EmployeeEmail class above, we wrote an addEmployeeEmailId() method that first checks whether an email ID is in valid format, and then adds it to a Map implementation. The isValidEmailId() method performs the email validation using a regular expression. We also wrote a getEmployeeEmailId() method to return an email ID from the Map, given a key.

To test the EmployeeEmail class, we will create a test class, EmployeeEmailTest and add test methods to it. Here, remember that the number of test methods to add and what they should do depends on the behavior of the EmployeeEmail class under test – not on the number of methods in it.

To start with, we will test that the getEmployeeEmailId() method returns true for a valid email ID and false for an invalid one with two test methods.

. . .
@Test
public void testValidEmailId() throws Exception {
    /*Arrange*/
    EmployeeEmail empEmail=new EmployeeEmail();
    /*Act*/
    boolean result = empEmail.isValidEmailId("[email protected]");
    /*Assert*/
    assertTrue("Valid email ID failed ", result );
}

@Test
public void testInvalidEmailId() throws Exception {
    /*Arrange*/
    EmployeeEmail empEmail=new EmployeeEmail();
    /*Act*/
    boolean result= empEmail.isValidEmailId("andy@testdomain");
    /*Assert*/
    assertFalse("Invalid email ID passed ", result);
}
. . .

In both the test methods above, we separated the test code into the AAA sections. In the first test method, we used the assertTrue() method as we expect isValidEmailId() to return true for the email ID, [email protected]. We also want to test that isValidEmailId() returns false for an invalid email ID. For that, we wrote the second test method and used assertFalse().

Couple of things to observe here. In both the assertion methods, we passed a String parameter as the identifying message for an assertion error. It’s common for programmers to set this message to describe the condition that should be met. Instead, to be meaningful, this message should describe what’s wrong if the condition isn’t met.

Also, you might be thinking “Why two separate test methods instead of a single method with both the assert methods?” Having multiple assert methods in a single test method will not cause any errors in tests, and you will frequently encounter such test methods. But a good rule to follow is: “Proper unit tests should fail for exactly one reason”, which sounds similar to the Single Responsibility Principle.  In a failed test method having multiple assertions, more effort is required to determine which assertion failed. Also, it is not guaranteed that all of the assertions took place. For an unchecked exception, the assertions after the exception will not execute and JUnit proceeds to the next test method. Therefore, it is generally a best practice to use one assertion per test method.

With the basics in place, let’s write the complete test class and use the following assertions:

  • assertEquals() and assertNotEquals(): Tests whether two primitives/objects are equal or not. In addition to the string message passed as the first parameter, these methods accept the expected value as the second parameter and the actual value as the third parameter- an important ordering commonly misused.
  • assertNull() and assertNotNull(): Tests whether an object is null or not null.
  • assertSame() and assertNotSame(): Tests whether two object references point to the same object or don’t.

EmployeeEmailTest.java

package guru.springframework.unittest.asserts;
import org.junit.Test;

import java.util.Map;

import static org.junit.Assert.*;

public class EmployeeEmailTest {
    @Test
    public void testValidEmailId() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        /*Act*/
        boolean result = empEmail.isValidEmailId("[email protected]");
        /*Assert*/
        assertTrue("Valid email ID failed ", result );
    }

    @Test
    public void testInvalidEmailId() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        /*Act*/
        boolean result= empEmail.isValidEmailId("andy@testdomain");
        /*Assert*/
        assertFalse("Invalid email ID passed ", result);
    }

    @Test
    public void testAddEmailId() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        empEmail.addEmployeeEmailId("Emp01","[email protected]");
        empEmail.addEmployeeEmailId("Emp02", "[email protected]");
        /*Act*/
        int size=empEmail.hashMap.size();
        /*Assert*/
        assertEquals("Incorrect collection size ", 2, size);
    }
    @Test
    public void testAddEmailIdWithDuplicateKey() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        empEmail.addEmployeeEmailId("Emp01","[email protected]");
        empEmail.addEmployeeEmailId("Emp02", "[email protected]");
        empEmail.addEmployeeEmailId("Emp02", "[email protected]");
        /*Act*/
        int size=empEmail.hashMap.size();
        /*Assert*/
        assertNotEquals("Duplicate key in collection ", 3, size);
    }

    @Test
    public void testGetExistingEmailId() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        empEmail.addEmployeeEmailId("Emp01","[email protected]");
        empEmail.addEmployeeEmailId("Emp02", "[email protected]");
        /*Act*/
        String val = empEmail.getEmployeeEmailId("Emp02");
        /*Assert*/
        assertNotNull("Returned null for existing employee", val);
    }

    @Test
    public void testGetNonExistingEmailId() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail=new EmployeeEmail();
        empEmail.addEmployeeEmailId("Emp01","[email protected]");
        empEmail.addEmployeeEmailId("Emp02", "[email protected]");
       /*Act*/
        String val = empEmail.getEmployeeEmailId("Emp05");
       /*Assert*/
        assertNull("Failed to return null for non existing employee", val);
    }

    @Test
    public void testIfObjectsAreSame() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail1=new EmployeeEmail();
        empEmail1.addEmployeeEmailId("Emp01","[email protected]");
        EmployeeEmail empEmail2=new EmployeeEmail();
        empEmail1.addEmployeeEmailId("Emp02", "[email protected]");
        /*Act*/
        Map map1=empEmail1.hashMap;
        Map map2=empEmail2.hashMap;
        map1= map2;
        /*Assert*/
        assertSame("Failed because objects are not same ", map1, map2);
    }

    @Test
    public void testIfObjectsAreNotSame() throws Exception {
        /*Arrange*/
        EmployeeEmail empEmail1=new EmployeeEmail();
        empEmail1.addEmployeeEmailId("Emp01","[email protected]");
        EmployeeEmail empEmail2=new EmployeeEmail();
        empEmail1.addEmployeeEmailId("Emp02", "[email protected]");
        /*Act*/
        Map map1=empEmail1.hashMap;
        Map map2=empEmail2.hashMap;
        /*Assert*/
        assertNotSame("Failed because objects are same ", map1, map2);
    }


}

 

In the EmployeeEmailTest class above:

  • Line 38: We used assertEquals() to test the collection size after adding two elements to it through addEmployeeEmailId().
  • Line 50: We used assertNotEquals() to test that the collection does not allow duplicate keys added through addEmployeeEmailId().
  • Line 62: We used assertNotNull() to test that getEmployeeEmailId() does not return null for an email ID present in the collection.
  • Line 74: We used assertNull() to test that getEmployeeEmailId() returns null for an email ID not present in the collection.
  • Line 89: We used assertSame() to test that two collection references point to the same collection object after assigning one to the other through the = operator.
  • Line 103: We used assertNotSame() to test that two collection references are not pointing to the same object.

When we run the test in IntelliJ, the output is:
Output in IntelliJ

As you can see from the output, all the tests passed as expected.

Note: The order in which JUnit executes test methods is not guaranteed, so don’t count on it.

If you go back and look into the test class, you will notice several lines of code in the Arrange part being repeated across the test methods. Ideally, they should be in a single place and get executed before each test. We can achieve this through the use of JUnit annotations, which we will look into next.

JUnit Annotations

You can use JUnit Annotations, introduced in JUnit 4, to mark and configure test methods. We have already used the @Test annotation to mark public void methods as test methods. When JUnit encounters a method annotated with @Test, it constructs a new instance of the class, and then invokes the method. We can optionally provide a timeout parameter to @Test to specify a time measured in milliseconds. If the test method takes longer to execute than the specified time, the test fails. This is particularly useful when you test against performance in terms of time. This code marks a method as a test method and sets the timeout to 100 milliseconds.

. . .
@Test(timeout = 100)
public void testDataAccessTimeout(){
    String val = empEmail.getEmployeeEmailId("Emp02");
}
. . .

Another important use of the @Test annotation is to test for exceptions. Suppose for a condition, a code throws an exception. We can use the @Test annotation to test whether the code indeed throws the exception when the condition is met. This code checks whether the getEmployeeEmailId() method throws an exception of type IllegalArgumentException when a non-String value is passed to it.

. . .
@Test(expected = IllegalArgumentException.class)
public void testForIllegalArgumentException()
{
    String val = empEmail.getEmployeeEmailId(1);

}
. . .

In addition to the @Test annotation, the other annotations are:

  • @Before: Causes a method to run before each test method of the class. You typically use this annotation to allocate resource, setup common initialization code, and load configuration files that the test methods require.
  • @After: Causes a method to run after each test method of the class. This method is guaranteed to run even if a @Before or @Test method throws an exception. Use this annotation to clean up initialization code and release any resource allocations done in @Before.
  • @BeforeClass: Causes a static method to run once and only once before any of the test methods in the class. This is useful in situations where you need to set up computationally expensive resources, say a server connection, a database, or even managing an embedded server for testing. As an example, instead of starting a server for each @Test method, start it once in a @BeforeClass method for all the tests in the class.
  • @AfterClass: Causes a static method to run once after all the test methods in the class completes. This method is guaranteed to run even if a @BeforeClass or @Test method throws an exception. Use this method to free one time resource initialization done in @BeforeClass.
  • @Ignore: Causes a test method to be ignored by JUnit. This can be useful when you have a complicated piece of code that is in transition, and you might want to temporarily disable some tests till that code is ready. Test runners of most IDEs report @Ignore tests as reminders during each test runs. This is essentially to mark tests as “there are things to be done”, which otherwise you might forget if you comment out the test method or remove the @Test annotation.

Here is an example of using all the JUnit annotations.

EmployeeEmailAnnotationsTest.java

package guru.springframework.unittest.asserts;
import org.junit.*;
import java.util.Map;
import static org.junit.Assert.*;

public class EmployeeEmailAnnotationsTest {
    EmployeeEmail empEmail;
    static int num;
    @BeforeClass
    public static void oneTimeSetup(){
     num=1;
     System.out.println("JUnit Call:"+num+" @BeforeClass oneTimeSetup");
    }
    @Before
    public void setup(){
        num+=1;
        System.out.println("JUnit Call:"+num+" @Before setUp");
        empEmail=new EmployeeEmail();
        empEmail.addEmployeeEmailId("Emp01","[email protected]");
        empEmail.addEmployeeEmailId("Emp02", "[email protected]");
    }
    @After
    public void cleanup()
    {
        num+=1;
        System.out.println("JUnit Call:" + num + " @After cleanup");
        empEmail.hashMap.clear();
    }
    @AfterClass
    public static void oneTimeCleanup()
    {
        num+=1;
        System.out.println("JUnit Call:"+num+" @AfterClass oneTimeCleanup");
        num=0;
    }
    @Test(timeout = 100)
    public void testDataAccessTimeout(){
        num+=1;
        System.out.println("JUnit Call:"+num+" @Test testDataAccessTimeout");
        String val = empEmail.getEmployeeEmailId("Emp02");
    }
    @Test
    @Ignore("Test code not ready")
    public void testWithMoreData(){
        /*ToDO: */
    }
    @Test(expected = IllegalArgumentException.class)
    public void testForIllegalArgumentException()
    {
        num+=1;
        System.out.println("JUnit Call:" + num + " @Test testForIllegalArgumentException");
        String val = empEmail.getEmployeeEmailId(1);

    }
}

The output on running the test in IntelliJ is:

JUnit Output in IntelliJ

JUnit Test Suites

If you have large numbers of test classes for different functional areas or modules, you can structure them into test suites. JUnit Test Suites are containers of test classes and gives you finer control over what order your test classes are executed in. JUnit provides org.junit.runners.Suite, a class that runs a group of test classes.
The code to create a test suite is:

EmployeeEmailTestSuite.java

package guru.springframework.unittest.testsuite;

import guru.springframework.unittest.asserts.EmployeeEmailAnnotationsTest;
import guru.springframework.unittest.asserts.EmployeeEmailTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
        EmployeeEmailTest.class,
        EmployeeEmailAnnotationsTest.class

})
public class EmployeeEmailTestSuite {
}

 

In the test suite class above, we wrote two annotations: @RunWith and @SuiteClasses. The @RunWith annotation instructs JUnit to use the Suite runner class and @SuiteClasses specifies the classes and their order that the Suite runner class should run. The test suite class is itself empty and acts only as a placeholder for the annotations.

The output on executing the test suite in IntelliJ is.

JUnit test suite Output in IntelliJ

Summary

JUnit Assertions not only make your code stable but also force you to think differently and think through different scenarios, which ultimately helps you to become better programmers. By understanding the purpose of different assertions and using them properly, testing becomes effective. But the question is “How many asserts per test method?”. It all comes down to the complexity of the method under test. For a method with multiple conditional statements, asserting the outcome for each condition should be done, while for a method performing a simple string manipulation, a single assertion should do. When developing unit tests with JUnit, it is considered a best practice that each test method is testing a specific condition, which will often lead to one assert per test method. Its not uncommon for a method under test to be associated with multiple test methods.
One assertion I have not covered in this post is assertThat(). It’s an important JUnit assertion which I will cover it in my next post on JUnit.

Unit Testing with the Spring Framework

While doing Enterprise Application Development with the Spring Framework and unit testing your code, you will be using lots of assertions. In addition to asserting the regular method behaviors, you will assert whether Spring beans are injected as expected by the Spring application context, whether dependencies between Spring beans are correctly maintained, and so on. While creating those tests ensure that they run fast, especially when testing is integrated in the build cycle. You will keep building your application as you code, and so you obviously won’t want your build to wait for a long running test to complete. If you do have such long running tests, put them in a separate test suite.

About jt

    You May Also Like

    5 comments on “Unit Testing with JUnit – Part 2

      Leave a Reply

      Your email address will not be published. Required fields are marked *

      This site uses Akismet to reduce spam. Learn how your comment data is processed.