JUnit 6 Nullability: @Nullable, @NotNull & @NullMarked

With the release of JUnit 6.0.0, the maintainers adopted JSpecify nullability annotations across all JUnit modules. It means that method parameters, return types, and other API elements in JUnit are now annotated to explicitly declare whether they accept or return null.

JUnit6 Logo

With the release of JUnit 6.0.0, the maintainers adopted JSpecify nullability annotations across all JUnit modules. It means that method parameters, return types, and other API elements in JUnit are now annotated to explicitly declare whether they accept or return null.

Thus, tests (or extensions) are safer, and you get compile-time or IDE-time hints instead of relying solely on runtime behavior.

If we annotate a parameter as @NonNull and accidentally pass null at runtime, the program will still run and may throw a NullPointerException only when we try to use that null value.

The annotation itself does not insert any null checks into the method.

1. Null‑related Annotations (via JSpecify in JUnit 6)

We can make use of the following annotation to specify the null-related behavior of the tests:

AnnotationDescription / Purpose
@Nullable– This marks a type of use (parameter type, return type, field type, etc.) as nullable.
– This value may be null.
– This informs IDEs/tools/static‑analysis and helps users know null‑safety contract.
@NonNull– This marks a type of use as non-null.
– This value must not be null.
– This ensures that callers / implementations treat it as non-null, giving stronger guarantees.
@NullMarked– This is a package‑/class‑/module-level annotation.
– This indicates that within the annotated scope, unannotated types should be treated as non-null by default (unless explicitly marked @Nullable).
– This helps reduce verbosity i.e. instead of marking every type as non-null, we mark only nullable ones.

Using @NullMarked can reduce annotation clutter. Instead of marking every non-null type explicitly, we can annotate whole classes/packages/modules and then only mark those that are nullable.

2. JUnit 6 @Nullable Example

In JUnit 6, @Nullable comes from the JSpecify annotations that JUnit 6 adopts to indicate whether a parameter or return value can be null. This doesn’t change test execution, but it helps IDE inspections, static analysis, and Kotlin interop.

Here’s a simple example showing @Nullable in a test:

import org.junit.jupiter.api.Test;
import org.jspecify.annotations.Nullable;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

class NullableExampleTest {

    // A simple method that may return null
    @Nullable
    String getNullableString(boolean returnNull) {
        if (returnNull) {
            return null;
        } else {
            return "Hello, JUnit 6!";
        }
    }

    @Test
    void testNullableReturn() {
        // Test case 1: method returns a string
        String result1 = getNullableString(false);
        assertEquals("Hello, JUnit 6!", result1);

        // Test case 2: method returns null
        String result2 = getNullableString(true);
        assertNull(result2); // @Nullable tells the IDE/static analysis that this can be null
    }

    @Test
    void testNullableParameter() {
        // @Nullable can also be used on parameters
        printMessage(null);
        printMessage("JUnit 6 is great!");
    }

    void printMessage(@Nullable String message) {
        if (message == null) {
            System.out.println("No message provided");
        } else {
            System.out.println(message);
        }
    }
}

The program output:

No message provided
JUnit 6 is great!

3. JUnit 6 @NonNull Example

In JUnit 6, @NonNull indicates that a method must not return null or a parameter must not be null. IDEs, static analysis tools, and Kotlin interop use this metadata to catch potential null-safety violations at compile time.

Here’s a simple JUnit 6 example:

import org.junit.jupiter.api.Test;
import org.jspecify.annotations.NonNull;

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

class NonNullExampleTest {

    // Method guarantees a non-null return value
    @NonNull
    String getNonNullMessage(boolean condition) {
        if (condition) {
            return "JUnit 6 rocks!";
        } else {
            return "Null is not allowed!"; // must return non-null
        }
    }

    @Test
    void testNonNullReturn() {
        String result = getNonNullMessage(true);
        assertEquals("JUnit 6 rocks!", result);

        String result2 = getNonNullMessage(false);
        assertEquals("Null is not allowed!", result2);
    }

    @Test
    void testNonNullParameter() {
        printMessage("Hello, JUnit 6!");
        // printMessage(null); // IDE/static analyzer will warn: null not allowed
    }

    // Parameter marked @NonNull → must not be null
    void printMessage(@NonNull String message) {
        // No need to check for null; tools assume non-null
        System.out.println(message.toUpperCase());
    }
}

The program output:

HELLO, JUNIT 6!

4. JUnit 6 @NullMarked Example

In JUnit 6, @NullMarked is a package‑, class‑, or module‑level annotation that indicates all unannotated types in the scope are non-null by default. You only need to annotate values that can be null with @Nullable. This reduces boilerplate and makes null-safety easier to manage.

Here’s a JUnit 6 example using @NullMarked:

import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullMarked;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

@NullMarked  // All unannotated types in this class are treated as @NonNull
class NullMarkedExampleTest {

    // Return type is non-null by default because of @NullMarked
    String getMessage(boolean condition) {
        return condition ? "JUnit 6 is awesome!" : "Null is not allowed!";
    }

    // Parameter is non-null by default
    void printMessage(String message) {
        System.out.println(message.toUpperCase());
    }

    // Only mark explicitly nullable parameters/returns
    @Nullable
    String getNullableMessage(boolean condition) {
        return condition ? "Some message" : null; // nullable allowed
    }

    @Test
    void testNonNullBehavior() {
        String msg = getMessage(true);
        assertEquals("JUnit 6 is awesome!", msg);

        // IDE or static analysis will warn if you try to pass null here:
        // printMessage(null); // ❌ warning
        printMessage(msg); // ✅ OK
    }

    @Test
    void testNullableBehavior() {
        String maybeNull = getNullableMessage(false);
        assertNull(maybeNull); // ✅ allowed because @Nullable
    }
}

The program output:

JUNIT 6 IS AWESOME!

5. Conclusion

JUnit 6’s adoption of JSpecify nullability annotations @Nullable, @NonNull, and @NullMarked represents a major improvement in type safety and developer guidance. While these annotations do not enforce runtime null checks, they significantly improve code safety, maintainability, and readability, helping developers write more reliable and expressive tests.

Happy Learning !!

Comments

Subscribe
Notify of
0 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.