In this short blog post I would like to explain how to avoid popular mistake when you write your first JUnit 5 test case in Groovy.

The example

Let’s start with the example. Here is our build.gradle file that adds JUnit 5 dependency:

Listing 1. build.gradle
plugins {
    id 'groovy'
    id 'idea'
}

repositories {
	jcenter()
}

dependencies {
    compile localGroovy()

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
}

test {
    useJUnitPlatform()
}

And here is our test class:

Listing 2. src/test/groovy/UnitTest.groovy
import org.junit.jupiter.api.Test

class UnitTest {

    @Test
    def shouldThrowAnException() {
        throw new RuntimeException()
    }
}

This test does nothing, but it’s fine. We just want to execute this test.

The problem

Let’s see what happens when we try to run this test:

% gradle test

BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed

Nothing happened. When we try to execute this test inside IntelliJ IDEA, we will see something like this:

groovy junit5 def mistake

What’s wrong?

The solution

The problem we shown above is caused by a single def keyword we used in shouldThrowAnException prototype. This is common mistake, made very often by people who have experience with Spock Framework (which tolerates def btw).

So, what is the problem with def keyword? Firstly, let’s take a look at the decompiled class:

Listing 3. Decompiled UnitTest.class file
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.junit.jupiter.api.Test;

public class UnitTest implements GroovyObject {
    public UnitTest() {
        CallSite[] var1 = $getCallSiteArray();
        super();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
    }

    @Test
    public Object shouldThrowAnException() {
        CallSite[] var1 = $getCallSiteArray();
        throw (Throwable)var1[0].callConstructor(RuntimeException.class);
    }
}

As you can see Groovy’s def keyword gets compiled to Object type in this case. OK, but what is wrong with that? Well, JUnit 5 method resolver uses IsTestMethod predicate which requires that test method returns void. Otherwise method annotated with @Test does not get resolved.

If we only replace def with void keyword:

Listing 4. Updated src/test/groovy/UnitTest.groovy
import org.junit.jupiter.api.Test

class UnitTest {

    @Test
    void shouldThrowAnException() {
        throw new RuntimeException()
    }
}

the test will execute and fail as expected:

% gradle test

> Task :test FAILED

UnitTest > shouldThrowAnException() FAILED
    java.lang.RuntimeException at UnitTest.groovy:7

1 test completed, 1 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///home/wololock/workspace/groovy-junit5/build/reports/tests/test/index.html

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 1s
2 actionable tasks: 2 executed

Szymon Stepniak

Groovista, Upwork's Top Rated freelancer, Toruń Java User Group founder, open source contributor, Stack Overflow addict, bedroom guitar player. I walk through e.printStackTrace() so you don't have to.