Junit is a popular open-source testing framework on the JVM. So it can work on java and kotlin projects. It’s the default testing framework for multiple other frameworks and tools. So let’s review some interesting features of Junit 5 which is to this date, the latest major version.
Setup
Let’s review the setup with the build.gradle
file to install the JUnit 5 dependency,
it’s easy to spot, it has jupiter in the name.
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
}
test {
useJUnitPlatform()
}
To run the rest with gradle test
we are going to use the useJUnitPlatform()
method
which will use junit under the hood to run the tests.
Assertion on thrown exception
When you want to test that the method throws an exception, you can use the assertThrows
method:
import org.junit.jupiter.api.Test;
class TestClass {
@Test
public void testException() {
assertThrows(RuntimeException.class, () -> example.methodThrowingException());
}
}
This one is not used often, so I tend to forget the syntax. The method call that will throw the exception
needs to be in a lambda, otherwise it will be executed before the assertThrows
and the test will fail.
Enhancing your tests
Display name
When running the tests, you will see the test’s name in the command line, which can become hard to
read, with longer names. You can use the @DisplayName
annotation to specify a custom name that does
not need to match the camel case syntax of the method name.
import org.junit.jupiter.api.DisplayName;
class TestClass {
@DisplayName("true should be true")
@Test
public void testThatHasADisplayName() {
assertTrue(true);
}
}
You can use the display name on the test class as well.
Nested tests
The nested tests are used to group together set of tests within the same class. This annotation needs to be used on an inner class.
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class TestClass {
@Nested
public class NestedTest {
@Test
public void testEqualNested() {
assertEquals(1, 1);
}
}
}
That way when running the tests, the tests result can be clearer. It is also practical if you need a specific setup for a group of tests within your test class, less duplication.
Customized error messages
There is a third argument in the assertEquals
that you can use to specify a custom error message.
This can be useful when there are multiple assertion, and it might not be too clear which one failed,
or what it means.
class TestClass {
@Test
public void testEqual() {
assertEquals("value", example.value, "Example's value is not equal to 'value'");
}
}
The expected should be according to the documentation the first argument, then the second one is the actual.
This has some importance, because it will influence the default error message you will receive. But in this case, we are overriding it with our own message.
Parameterized tests
Parametrized tests are used to run the same test with different values.
With values
You can pass the expected values directly via the @ValueSource
annotation.
But this one only accepts primitive types (e.g. ints
, strings
)
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class TestClass {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
public void testEqualParameterizedByValues(int input) {
assertEquals(0, input * 0);
}
}
With parametrized tests, you need a test method with parameters of the exact same type as the one in the @ValueSource
annotation.
The test will run three times in this example, as many times as there are values.
With method
If you need to pass multiple or more complex values, you can use a method to provide the values.
In this case the method needs to be static
and we are using the junit Arguments
interface as return type.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class TestClass {
@ParameterizedTest
@MethodSource("getValues")
public void testEqualParameterizedByMethod(int input, String text) {
assertEquals(0, input * 0, text);
}
private static Stream<Arguments> getValues() {
return Stream.of(
Arguments.of(2, "two"),
Arguments.of(3, "tree"),
Arguments.of(4, "four")
);
}
}
Since we have two values per arguments, the test method will need two parameters with the matching type in the same order. In this context, the test will be run three times, one per argument.
Other Junit features
Methods to set up / teardown each suits
If you need to run some setup and teardown method before and after all the tests in the class, you can use those annotations:
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
class TestClass {
@BeforeAll
static void initAll() {
System.out.println("before all");
}
@AfterAll
static void tearDownAll() {
System.out.println("after all");
}
}
They need to be static, and are usually used when you want to set up a dependency for the test suit.
Methods to init / reset between each test
To run some setup and teardown method before and after each test, you can use those annotations:
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
class TestClass {
@BeforeEach
void init() {
System.out.println("before each");
}
@AfterEach
void tearDown() {
System.out.println("after each");
}
}
They are run in between tests, and are usually used when you want to reset the state of the test.