Unit Testing with JUnit – Part 2 (original) (raw)

Last Updated on June 16, 2019 by

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:

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]](/cdn-cgi/l/email-protection). 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:

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]](/cdn-cgi/l/email-protection)");
    empEmail.addEmployeeEmailId("Emp02", "[[email protected]](/cdn-cgi/l/email-protection)");
    /*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]](/cdn-cgi/l/email-protection)");
    empEmail.addEmployeeEmailId("Emp02", "[[email protected]](/cdn-cgi/l/email-protection)");
    empEmail.addEmployeeEmailId("Emp02", "[[email protected]](/cdn-cgi/l/email-protection)");
    /*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]](/cdn-cgi/l/email-protection)");
    empEmail.addEmployeeEmailId("Emp02", "[[email protected]](/cdn-cgi/l/email-protection)");
    /*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]](/cdn-cgi/l/email-protection)");
    empEmail.addEmployeeEmailId("Emp02", "[[email protected]](/cdn-cgi/l/email-protection)");
   /*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]](/cdn-cgi/l/email-protection)");
    EmployeeEmail empEmail2=new EmployeeEmail();
    empEmail1.addEmployeeEmailId("Emp02", "[[email protected]](/cdn-cgi/l/email-protection)");
    /*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]](/cdn-cgi/l/email-protection)");
    EmployeeEmail empEmail2=new EmployeeEmail();
    empEmail1.addEmployeeEmailId("Emp02", "[[email protected]](/cdn-cgi/l/email-protection)");
    /*Act*/
    Map map1=empEmail1.hashMap;
    Map map2=empEmail2.hashMap;
    /*Assert*/
    assertNotSame("Failed because objects are same ", map1, map2);
}

}

In the EmployeeEmailTest class above:

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:

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.