JUnit 5 Demo – Maven

Greetings, JUnit fans!

Because tool vendors, in order to get the most out of JUnit, used
internal APIs, this caused JUnit maintainers some headaches in moving
JUnit forward, as explained in this article by Nicolai
Parlog.

JUnit 5 breaks new ground mainly because of its
built in separation of concerns into two areas:

  1. An API used to write tests
  2. A mechanism to discover and run those tests.

For that reason, JUnit 5 was split into three sub
projects:

  • JUnit Jupiter – the API to write tests
  • JUnit Platform – the API to discover and run tests
  • JUnit Vintage – provide backward compatibility to run JUnit 3 and 4 tests.

For this post, I’ll use Eclipse, but you should be able to use any IDE you like.

The code for this post is available from Github at this link: https://github.com/makotogo/JUnit5MavenDemo

I’ll clone the code first:

$ cd ~/home/projects/learn
$ git clone https://github.com/makotogo/JUnit5MavenDemo

Next, I’ll start Eclipse, and import the code.

  1. Go to File > Import…
  2. Choose Maven > Existing Maven Projects.
  3. Navigate to the folder where you cloned the github repo (in my case it’s ~/home/projects/learn/JUnit5MavenDemo).
  4. Click Finish.

The class under test is very simple:

package com.makotojava.learn.junit5.demo;

public class App {
  public long add(long[] operands) {
    // Compute the sum
    long ret = 0;
    if (operands == null) {
      throw new IllegalArgumentException("Operands argument cannot be null");
    }
    if (operands.length == 0) {
      throw new IllegalArgumentException("Operands argument cannot be empty");
    }
    for (long operand : operands) {
      ret += operand;
    }
    return ret;
  }
}

Next, let’s look at the JUnit5App class. I’ll show it in pieces and explain the pieces as I go. Please refer to the full code listing in your IDE if you need more context.

package com.makotojava.learn.junit5.demo;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
@DisplayName("Testing using JUnit 5")
public class JUnit5AppTest {
  private App classUnderTest;
  @BeforeAll
  public static void init() {
    // Do something before ANY test is run in this class
  }

  @AfterAll
  public static void done() {
    // Do something after ALL tests in this class are run
  }

  @BeforeEach
  public void setUp() throws Exception {
    classUnderTest = new App();
  }

  @AfterEach
  public void tearDown() throws Exception {
    classUnderTest = null;
  }

Notice the JUnit package names all begin with org.junit.jupiter.api. The JUnit package names have changed.

Next notice the @RunWith annotation and its JUnitPlatform.class argument. This allows tests written using JUnit Jupiter to run with a tool (like Eclipse) that does not yet natively support JUnit 5.

There is also the @DisplayName annotation, which tells JUnit to use the String argument (e.g., “Testing using JUnit 5”) rather than the default, which is the fully qualified test class name.

Then there are these annotations:

  1. BeforeAll – tells JUnit to run this method once before all test methods have run.
  2. BeforeEach – tells JUnit to run this method before each test method.
  3. AfterEach – tells JUnit to run this method after each test method.
  4. AfterAll – tells JUnit to run this method once after all test methods have run.

Next, let’s look at the testAdd() method:

  @Test
  @DisplayName("When numbers are > 0")
  public void testAdd() {
    assertAll(
        () -> {
          //
          // Test #1
          long[] numbersToSum = { 1, 2, 3, 4 };
          long expectedSum = 10;
          long actualSum = classUnderTest.add(numbersToSum);
          assertEquals(expectedSum, actualSum);
        },
        () -> {
          //
          // Test #2
          long[] numbersToSum = new long[] { 20, 934, 110 };
          long expectedSum = 1064;
          long actualSum = classUnderTest.add(numbersToSum);
          assertEquals(expectedSum, actualSum);
        },
        () -> {
          //
          // Test #3
          long[] numbersToSum = new long[] { 2, 4, 6 };
          long expectedSum = 12;
          long actualSum = classUnderTest.add(numbersToSum);
          assertEquals(expectedSum, actualSum);
        });
  }

First, we tell JUnit that this is a test method by annotating it with the @Test annotation.

Next, notice the assertAll() method. This actually runs three separate tests, each of which calls assertEquals to verify the actual result matches the expected result. No big deal, right? Well, if you’ve used JUnit 4, you know that if the first of these assertEquals() calls fails then the next two are simply not run. This is not the case with assertAll. All of the statements within the lambda are run, and the results are reported as a group.

Finally, I want to point out a new JUnit 5 annotation: @Nested. Take a look at this code from JUnit5AppTest.java:

  @Nested
  @DisplayName("When numbers to add are < 0")
  class NegativeNumbersTest {

    private App classUnderTest;

    @BeforeEach
    public void setUp() throws Exception {
      classUnderTest = new App();
    }

    @AfterEach
    public void tearDown() throws Exception {
      classUnderTest = null;
    }

    @Test
    @DisplayName("Three tests with numbers  {
            //
            // Test #1
            long[] numbersToSum = { -1, -2, -3, -4 };
            long expectedSum = -10;
            long actualSum = classUnderTest.add(numbersToSum);
            assertEquals(expectedSum, actualSum);
          },
          () -> {
            //
            // Test #2
            long[] numbersToSum = { -20, -934, -110 };
            long expectedSum = -1064;
            long actualSum = classUnderTest.add(numbersToSum);
            assertEquals(expectedSum, actualSum);
          },
          () -> {
            //
            // Test #3
            long[] numbersToSum = new long[] { -2, -4, -6 };
            long expectedSum = -12;
            long actualSum = classUnderTest.add(numbersToSum);
            assertEquals(expectedSum, actualSum);
          });
    }
  }

This annotation lets us create an inner test class within the main test class. This keeps the code more structured and orderly, while keeping code together that tests the same class under test.

The @DisplayName annotation helps as well, and the results of any tests run in the @Nested class are reported “indented” relative to the main test class.

The nesting can be arbitrary also. So you can create @Nested classes within @Nested classes (within @Nested classes, and so forth) to an arbitrary level of nesting, as you see fit, and as makes sense for what you’re trying to test.

One last thing I want to point out about @Nested classes: they may each have their own @BeforeEach and @AfterEach lifecycle methods. So if you need to do special initialization, for example, you can do that outside of the main class’ @BeforeEach and @AfterEach callbacks (which still run, by the way, so keep that in mind). Because of the way the @Nested inner classes are created, they may NOT have their own @BeforeAll and @AfterAll lifecycle callbacks. Sorry.

I hope you enjoyed this brief look at JUnit 5. Please check out the video below on my YouTube channel for a more complete look at the code.

Thanks for reading!

–jsp

Advertisements

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: