Using JUnit5 – Part 2 – Testing Basics

java, junit5

Published: 2019-07 (July 2019)
Relevant for: JUnit 5.5.0

JUnit5 Blog Series

Part 1 – Introduction
Part 2 – Test Basics

In Part 1 of the blog series, we looked at several annotations used in JUnit5. We covered test methods as well as lifecycle methods.

This post will share examples of a JUnit test which has a few of these annotations.

We will use a recently created code kata for the examples.

Testing

Marking a method as a Test

Tests in JUnit5 are annotated with the @Test annotation. Unlike prior versions of JUnit, the JUnit5 @Test annotation does not have any attributes. Prior versions supported extensions via attributes, while JUnit5 fosters a custom annotation based extension (more on this in a future blog).

Example: @Test (https://github.com/c-guntur/java-katas/blob/baseline/java-datetime/src/solutions/java/none/cvg/datetime/STest1InstantAndDateInteropTest.java#L43)

    @Test
    @Tag("PASSING")
    @Order(1)
    public void verifyInstantAndDateHaveSameEpochMilliseconds() {

        // DONE: Replace the Instant.now() with an instant from classicDate.
        //  Use a Date API that converts Date instances into Instant instances.
        //  Check: java.util.Date.toInstant()
        Instant instant = classicDate.toInstant();

        // DONE: Replace the "null" below to get milliseconds from epoch from the Instant
        //  Use an Instant API which converts it into milliseconds
        //  Check: java.time.Instant.toEpochMilli()
        assertEquals(Long.valueOf(classicDate.getTime()),
                instant.toEpochMilli(),
                "Date and Instant milliseconds should be equal");
    }

Assertions

Assertions are how testing is conducted. Several types of assertions exist for testing. Some examples include:

  • assertTrue / assertFalse
  • assertEquals / assertNotEquals / assertSame / assertNotSame
  • assertNull / assertNotNull
  • assertArrayEquals / assertIterableEquals / assertLinesMatch
  • assertThrows / assertNotThrows
  • assertAll
  • assertTimeout / assertTimeoutPreemptively
  • fail

There are several polymorphs for most of the methods listed above. JUnit5 aggregates all such assertions as static methods in a single factory utility aptly named Assertions (https://junit.org/junit5/docs/5.5.0/api/org/junit/jupiter/api/Assertions.html).

Assertions are mostly unary or binary (There are assertions, with other arities, that are less commonly used)

Unary assertions are usually boolean condition evaluators. assertTrue or assertNull are good examples of unary assertions. The expectation in such cases is built into the actual assertion method name. A second optional parameter for unary assertions is a message that is returned in case of an assertion failure and exists to provide more meaningful readable failure details.

Binary assertions typically have an expected value (a known), an actual value (evaluated) and an optional third parameter of message (in case of the expectation not being met). assertEquals and assertSame are good examples of binary assertions.

Typically, unit tests statically import the assertions required for the given tests in a test class. An example of such an assertion is the assertEquals method as shown below.

Example: Assertion (https://github.com/c-guntur/java-katas/blob/baseline/java-datetime/src/solutions/java/none/cvg/datetime/STest1InstantAndDateInteropTest.java#L56)

    @Test
    @Tag("PASSING")
    @Order(1)
    public void verifyInstantAndDateHaveSameEpochMilliseconds() {

        // DONE: Replace the Instant.now() with an instant from classicDate.
        //  Use a Date API that converts Date instances into Instant instances.
        //  Check: java.util.Date.toInstant()
        Instant instant = classicDate.toInstant();

        // DONE: Replace the "null" below to get milliseconds from epoch from the Instant
        //  Use an Instant API which converts it into milliseconds
        //  Check: java.time.Instant.toEpochMilli()
        assertEquals(Long.valueOf(classicDate.getTime()),
                instant.toEpochMilli(),
                "Date and Instant milliseconds should be equal");
    }

See also: Static Import (https://github.com/c-guntur/java-katas/blob/baseline/java-datetime/src/solutions/java/none/cvg/datetime/STest1InstantAndDateInteropTest.java#L18)

import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Date;
import java.util.TimeZone;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import static none.cvg.datetime.LenientAssert.assertAlmostEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

NOTE: Assertion parameter ordering in JUnit 5 is different from the order in prior versions. In my opinion, the current parameter arrangement makes a lot more sense.

Filtering and Categorizing Tests

Tags

Tags are a means to categorize test methods and classes. Tagging also leads to discovery and filtering of tests. Tagging is done by annotating the class or method with an @Tag annotation. More on filtering in another blog of this series.

Example: @Tag (https://github.com/c-guntur/java-katas/blob/baseline/java-datetime/src/solutions/java/none/cvg/datetime/STest1InstantAndDateInteropTest.java#L62)

    @Test
    @Tag("PASSING")
    @Order(1)
    public void verifyInstantAndDateHaveSameEpochMilliseconds() {

        // DONE: Replace the Instant.now() with an instant from classicDate.
        //  Use a Date API that converts Date instances into Instant instances.
        //  Check: java.util.Date.toInstant()
        Instant instant = classicDate.toInstant();

        // DONE: Replace the "null" below to get milliseconds from epoch from the Instant
        //  Use an Instant API which converts it into milliseconds
        //  Check: java.time.Instant.toEpochMilli()
        assertEquals(Long.valueOf(classicDate.getTime()),
                instant.toEpochMilli(),
                "Date and Instant milliseconds should be equal");
    }

Assumptions

Assumptions are conditions that determine if the rest of the test code block should be either evaluated or aborted. Not meeting an assumption will not cause the code block conditioned by it, to fail. It would rather simply abort execution of such a code block. Some assumption methods:

  • assumeTrue / assumeFalse
  • assumeThat

Similar to assertions, assumptions are grouped as static methods, in a factory utility class, Assumptions (https://junit.org/junit5/docs/5.5.0/user-guide/#writing-tests-assumptions).

class TestWithAssumptions {

    @Test
    void testOnlyOnHost123() {
        assumeTrue("host123".equals(System.getenv("HOSTNAME")));
        // remainder of test
    }

    @Test
    void testOnHost123OrAbortWithMessage() {
        assumeTrue("host123".equals(System.getenv("HOSTNAME")),
            () -> "Aborting test: not on host123");
        // remainder of test
    }

}

Typically, unit tests statically import the assumptions required for the given tests in a test class.

There is no current example of an assumption in the code kata.

Ordering Tests

Test execution order

As stated in the previous part of the blog series, I reserve my opinions of ordering the sequence of test executions. It is useful in certain cases, such as code katas. Test ordering requires either one or two steps depending on the type of ordering.

NOTE: If no order is specified for a test class, JUnit 5 looks for instructions from parent class hierarchy and if still none found, will order tests in a deterministic but non-obvious manner.

Instructing JUnit to order tests

An annotation on the test class is needed to instruct JUnit5 to order tests. This annotation is called the @TestMethodOrder. This annotation accepts an attribute of type MethodOrderer. There are three default implementations that exist:

  1. Alphanumeric – uses String::compareTo to order execution of test methods
  2. OrderAnnotation – uses the @Order annotation on each test method to determine order. The Order annotation accepts a int attribute that specifies the ranking.
  3. Random – uses a random order either simply from System.nanoTime() or in combination with a custom seed.

More custom orders can be created by implementing the MethodOrderer interface.

Example: @TestMethodOrder (https://github.com/c-guntur/java-katas/blob/baseline/java-datetime/src/solutions/java/none/cvg/datetime/STest1InstantAndDateInteropTest.java#L31)

/**
 * The tests in this class aim to show interoperability between
 * `java.util.Date` and the newer `java.time.Instant`.
 *
 * @see Instant
 * @see Date
 * @see LenientAssert
 */
@DisplayNameGeneration(DateTimeKataDisplayNames.class)
@DisplayName("Instant And Date Interoperability")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class STest1InstantAndDateInteropTest {

Extra Step (For OrderAnnotation only): Adding an Order via annotations

In addition to the above annotation instructing JUnit to order test methods, an additional annotation is needed if the OrderAnnotation orderer is specified. The @Order annotation accepts an integer that specifies the ascending order of execution.

Example: @Order (https://github.com/c-guntur/java-katas/blob/baseline/java-datetime/src/solutions/java/none/cvg/datetime/STest1InstantAndDateInteropTest.java#L45)

    @Test
    @Tag("PASSING")
    @Order(1)
    public void verifyInstantAndDateHaveSameEpochMilliseconds() {

        // DONE: Replace the Instant.now() with an instant from classicDate.
        //  Use a Date API that converts Date instances into Instant instances.
        //  Check: java.util.Date.toInstant()
        Instant instant = classicDate.toInstant();

        // DONE: Replace the "null" below to get milliseconds from epoch from the Instant
        //  Use an Instant API which converts it into milliseconds
        //  Check: java.time.Instant.toEpochMilli()
        assertEquals(Long.valueOf(classicDate.getTime()),
                instant.toEpochMilli(),
                "Date and Instant milliseconds should be equal");
    }

That’s a wrap of part two of this blog series. The next blog will include customizing tests with DisplayNames and writing a custom DisplayNameGeneration. Happy coding !

Using JUnit5 – Part 1 – An Introduction

java, junit5

Published: 2019-07 (July 2019)
Relevant for: JUnit 5.5.0

JUnit5 Blog Series

Part 1 – Introduction
Part 2 – Test Basics

Code Katas are a great way of teaching programming practices. The effectiveness of a code kata is to “solve” something repeatedly in order to gain a “muscle memory” of sorts on the subject matter.

Nothing stresses repeatability more than unit tests. Code Katas thus, in many cases can be associated with or run via unit tests.

Many of us have been long used to JUnit4 as a formidable unit testing framework. This blog is not going to be a comparison between JUnit4 and JUnit5, but you will notice some differences as italicized text.

Let us explore JUnit5 as it was used for a recent code kata, this is how I learnt using JUnit 5 !

JUnit5 Logo

JUnit5 dependencies

JUnit5 can be added as a single maven dependency:

            <dependency>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter</artifactId>
                <version>${junit5.version}</version>
            </dependency>

The equivalent gradle dependency can be inferred.

What is JUnit5 and What is Jupiter?

JUnit5 is made of three separate parts:

  1. JUnit5 Platform: Provides a TestEngine and a testing platform for the JVM.
  2. JUnit5 Jupiter: Programming and extension model for JUnit5 tests.
  3. JUnit Legacy: Backward compatibility TestEngine for JUnit 3 and 4.

Read more about this at the JUnit5 User Guide (https://junit.org/junit5/docs/current/user-guide/).

JUnit5 Basics

Base package for JUnit 5 is: org.junit.jupiter. Most unit test annotations are located at: org.junit.jupiter.apipackage (in the junit-jupiter-api module). Methods in JUnit5 Test can be typically grouped into :

  1. Test methods: Methods that are run as unit tests.
  2. Lifecycle methods: Methods that are executed as before or after one or more (or all) test methods.

Basic Annotations

@Test: Identifies a method as a test method. Unlike prior versions, this annotation does not have attributes. #TestMethod

@Disabled: An annotation to ignore running a method marked as @Test. #TestMethod

@BeforeEach: A setup method that is run before execution of each test. #LifecycleMethod

@BeforeAll: A static setup method run once before execution of all tests. #LifecycleMethod

@AfterEach: A teardown method this is run after execution of each test. #LifecycleMethod

@AfterAll: A static teardown method run once after execution of all tests. #LifecycleMethod

Other Annotations

@Tag: A category or grouping annotation. This is very useful specially when filtering which tests should be run via build tools such as maven. Example in another blog in this series.

@DisplayName: A string that can represent the class or method in the JUnit exection results instead of the actual name. Example in another blog in this series.

@DisplayNameGeneration: A class that can generate class and method names based upon conditions. Examples in another blog in this series.

Custom annotations: It is quite simple to create custom annotations and inherit the behavior.

JUnit5 Conditional Control of Test Methods

Operating System Conditions

@EnabledOnOs: Enable a test to run on a specific array of one or more operating systems.

@DisabledOnOs: Disable a test to run on a specific array of one or more operating systems.

Java Runtime Environment Conditions

@EnabledOnJre: Enable a test to run on a specific array of one or more Java Runtime Environments.

@DisabledOnJre: Disable a test to run on a specific array of one or more Java Runtime Environments.

System Property Conditions

@EnabledIfSystemProperty: Enable a test to run if a System Property matches the condition attributes.

@DisabledIfSystemProperty: Disable a test to run if a System Property matches the condition attributes.

Ordering Test method execution

JUnit5 allows for ordering test method execution. This causes mixed feelings for me.

My feelings: Ordering methods may lead to some developers building out dependent tests where the result of one test is needed for the next to run or pass. Tests should be independent. That said, it is an incredibly useful a feature when used in code katas where the run of tests may have to follow a certain sequence. In the past, I used to solve this by naming my test methods with some numeral-inclusive prefix and sort the results alphabetically. With great power, comes great responsibility.

@TestMethodOrder: Test methods can be ordered when the Test class is marked with this annotation.

@Order: Each test method can then include an Order annotation that includes a numeric attribute.

These were some of the basic that we covered. The next blog in this series will show examples of how we use these features.