JUnit Practices 1 : Let the Tests Talk

Each variant of the assertion in JUnit has a distinct purpose, used in a right context the assertions yield useful information on test failures. It is possible to construct tests with an assertion that is intend for a different purpose and make it work as expected; when we do so we may possibly loosing the useful information that a failure trace can give us.

Let us explore a couple of test scenarios and see what it yields to

Testing for equality, lets take case of Strings, the equality can be tested in two ways

  1. assertEquals(expected_string, actual_string)
    @Test
    public void testStringWithAssertTrue() {
    assertTrue("abc".equals("vdc"));
    }
  2. asserTure(expected_string.equals(actual_string))
     @Test
    public void testStringWithAssertTrue() {
    assertTrue("abc".equals("vdc"));
    }

Both of these will serve the purpose of testing, but the question is will they give the same insight on a test failure?
The test failure trace for the approach 1 is
java.lang.AssertionError at org.junit.Assert.fail(Assert.java:86)
at org.junit.Assert.assertTrue(Assert.java:41)
at org.junit.Assert.assertTrue(Assert.java:52) 

failure trace for the approach 2 is
org.junit.ComparisonFailure: expected:<[ab]c> but was:<[vd]c>
at org.junit.Assert.assertEquals(Assert.java:115)
at org.junit.Assert.assertEquals(Assert.java:144) 

The second approach gives a better perspective of the failure; it clearly says whats expected and what is actual.

Use assertEquals(obj1, obj2) to test the equality, avoid assertTrue(obj1.equals(obj2))

Testing for identity equality, i.e. testing if two objects are same. Lets say we are testing a singleton class. We could possibly assert it in two ways

  1. assertTrue(singleton1 == singleto2);
    @Test
    public void faultySingletonMultiThread () throws InterruptedException, ExecutionException {
    ExecutorService exec = Executors.newFixedThreadPool(2);
    Callable job = () -> FaultySingleton.getInstance();
    Future fSingleTonFuture1 = exec.submit(job);
    Future fSingleTonFuture2 = exec.submit(job);
    assertTrue(fSingleTonFuture1.get() == fSingleTonFuture2.get());
    } 
  2. assertSame(singleton1, singleto2);
    @Test
    public void faultySingletonMultiThread () throws InterruptedException, ExecutionException {
    ExecutorService exec = Executors.newFixedThreadPool(2);
    Callable job = () -> FaultySingleton.getInstance();
    Future fSingleTonFuture1 = exec.submit(job);
    Future fSingleTonFuture2 = exec.submit(job);
    assertSame(fSingleTonFuture1.get(), fSingleTonFuture2.get());
    } 

The failure trace of approach 1 is
java.lang.AssertionError
at org.junit.Assert.fail(Assert.java:86)
at org.junit.Assert.assertTrue(Assert.java:41)
at org.junit.Assert.assertTrue(Assert.java:52)

and the failure trace of approach 2 is
java.lang.AssertionError: expected same:<org.practprogrammer.junitpractices.FaultySingleton@6477463f> was not:<org.practprogrammer.junitpractices.FaultySingleton@3d71d552>
at org.junit.Assert.fail(Assert.java:88)

The failure trace of approach 2 is more clear and one can clearly understand that the test expects the same object.

Use assertSame(obj1, obj2) to test the identity equality, avoid assertTrue(obj1 == obj2)

To conclude,

In almost all the scenarios, it is possible to use assertTrue to verify the results. By doing so, we are muting the test, we aren’t letting the failure trace talk. Use appropriate assertion variant and let the tests talk .

The source code for the examples used in this article can be found @ my github

What’s up with JUnit 5

JUnit 5 is not yet a GA release but it has started making the buzz around with its all new features, especially the Java8 enthusiasts like me couldn’t wait for the GA release to start using. Architecturally Junit 5 is a whole different animal than its predecessor; I will not talk about the architecture in this article instead will give a glimpse of how this new version of JUnit can improve the tests, if you are interested in understanding the JUnit 5 architecture I suggest a read of JUnit 5 user guide. 

The following are the changes or improvements over JUnit 4

  • There’s no difference in the way of writing basic tests, remains same as the JUnit 4
  • @Before and @After annotations are given new names @BeforeEach and @AfterEach without any change in the work they do
  • In the same lines as above @BeforeClass and @AfterClass are given new names @BeforeAll and @AfterAll; these are also unchanged with respect to the work they do or the constraints the the methods using these annotation need to meet
  • @Ignore got a new as well, @Disabled
  • Categorizing the tests has got a new annotation @Tag in place of @Category

The following are the new features added in Junit 5

  • A couple of years back we had a situation where the business team wanted to have a look at our unit test report with meaningful names. Then we had to create a custom annotation that holds the name of the test cases and used our implementation of RunListener to generate a meaningful report. JUnit 5 now provides the annotation @DisplayName that can be used to provide a name to the test. May sound like a trivial improvement but it really makes it’s case
  • @RepeatedTest annotation that is used to repeat a single test several times is another cool feature that comes handy when number of execution can change the state of an Object or to verify if the repeated execution causes any resource clog
  • Dependency Injection for constructors and the methods, which means the test methods as well as the methods annotated with @BeforeXXX and @AfterXXX can now take the arguments with the default runner. No need to fall back Parameterized class or the TestNG’s DataProviderRunner
  • Parameterized tests are now more flexible; one class can have different data sources, different methods can use different data sources. The test needs to be annotated with @ParameterizedTest and then with a source annotation like @ValueSource. There are different source annotations for different type of sources, will touch upon them in the upcoming articles.
  • Dynamic tests is another exciting feature. The tests are created at run time from a collection or stream of values as below. The dynamic test needs to be annotated with @TestFactory (Test Factory generated the tests at the run time). I will touch upon this in detail with code examples in upcoming articles

@TestFactory
Stream dynamicTestsFromStream() {
return Stream.of("your", "input/output", "values").map(
str -> dynamicTest("test" + s, "tr, () -> { /* A lambda of executable; This is where the test implementation goes */ }));
}

Note: It is to be noted that unlike the regular tests, before and after exeution of the dynamic tests the methods annotated with @BeforeXXX and @AfterXXX are not executed 

  • With JUnit 5, we can write a test class nested inside another test class. Inner class needs to be annotated with @Nested. This is particularly useful to group the tests but don’t want to write whole new outer class.

Hope you enjoyed reading this article, will comeback with the detailed articles about each of these cool features.