JUnit 6 Nullability: @Nullable, @NonNull and @NullMarked Explained

JUnit 6 adopted JSpecify nullability annotations across its entire API surface. Every method parameter, return type, and field in JUnit’s public API is now explicitly annotated to declare whether it can be null. The result is stronger IDE guidance, better static analysis, and safer Kotlin interop β€” all without changing how your tests run.

This guide explains the three annotations you will encounter β€” @Nullable, @NonNull, and @NullMarked β€” with complete, runnable examples for each.

Why JUnit 6 Adopted JSpecify

Before JUnit 6, the framework’s API gave no formal signal about which parameters or return values could be null. Developers relied on documentation, runtime exceptions, or convention. JUnit 6 fixed this by adopting JSpecify β€” a vendor-neutral, standardised set of null-safety annotations that IDEs (IntelliJ, Eclipse), static analysers (NullAway, Error Prone, SpotBugs), and Kotlin’s compiler all understand.

One important point to understand upfront: these annotations do not insert null checks at runtime. They are compile-time and tooling contracts. If you pass null to a @NonNull parameter at runtime, the JVM will not throw an exception at the call site β€” it will throw a NullPointerException only when that value is actually dereferenced. The annotations shift detection earlier, to your IDE and CI pipeline.

Continue reading JUnit 6 Nullability: @Nullable, @NonNull and @NullMarked Explained

Illustrating Binary Countdown Protocol with C++ Program

The Binary Countdown Protocol is a contention-resolution MAC (Medium Access Control) protocol used on shared broadcast channels. When multiple stations want to transmit simultaneously, each station broadcasts its address as a binary number, bit by bit from the most significant bit (MSB) downward. Stations with a 0 bit at a position where another station has a 1 bit drop out of the contention. The station whose complete binary address survives the entire comparison wins the channel and transmits its frame. This guarantees that the station with the highest binary address always wins each contention round.

This C++ program simulates the Binary Countdown Protocol. Each frame is treated as an 8-bit binary number. The program converts each frame to its decimal equivalent (which represents the station’s binary address), then announces the frames in descending priority order β€” highest decimal value first β€” as they would be granted channel access.

Continue reading Illustrating Binary Countdown Protocol with C++ Program

10 AI Prompts to Debug and Fix JUnit 6 Test Failures

The test failure that taught me to write better debugging prompts was a flaky integration test that failed roughly one CI run in five. I pasted the assertion error into an AI assistant and got back five generic guesses, none right. Then I pasted the complete picture β€” full stack trace, the whole test class, the production method, and the fact that it only failed in parallel runs β€” and the diagnosis (shared static state in a test fixture) came back in one reply. The lesson: with debugging, the prompt is mostly evidence collection.

This post collects 10 prompts for diagnosing and fixing broken, flaky, or misbehaving JUnit 6 tests, each structured around the evidence that particular failure type needs β€” so the AI diagnoses the root cause instead of guessing. Tested with Claude Sonnet 4 and GPT-4o against JUnit 6.0, Mockito 5.x, and Spring Boot 3.4 failures.

For complementary reading, see Debugging JUnit 6 Tests: Fix Failures Like a Pro and How to Fix Flaky Tests in JUnit 6.

Continue reading 10 AI Prompts to Debug and Fix JUnit 6 Test Failures

10 AI Prompts to Optimise and Update Existing JUnit 6 Tests

Existing test suites accumulate technical debt just like production code. Tests that were written quickly become brittle, slow, hard to read, or duplicated. Optimising and updating your JUnit 6 tests is not just housekeeping β€” it directly determines whether your test suite remains a trusted safety net or becomes a maintenance burden that developers work around.

This post gives you 10 targeted AI prompts designed to refactor, improve, and modernise existing JUnit 6 test code. Each prompt addresses a specific quality problem β€” from brittle mocking to slow Spring contexts to missing boundary cases β€” and produces immediately usable improved test code.

For foundational context, see Writing Maintainable Tests in JUnit 6 and Refactoring Legacy Tests to JUnit 6.

Continue reading 10 AI Prompts to Optimise and Update Existing JUnit 6 Tests

10 AI Prompts to Generate JUnit 6 Tests for New Projects

The first time I asked an AI assistant to generate tests for a service class, it gave me fifteen green tests that verified almost nothing β€” assertNotNull on every line, mocks returning values no production system would produce, and not a single boundary case. The tests passed; they just didn’t protect anything. What changed the output quality wasn’t a better model β€” it was a better prompt: one that pinned the JUnit version, demanded specific annotations, listed the scenarios that had to be covered, and forced meaningful assertions.

This post collects the 10 prompts for generating JUnit 6 tests that came out of that process β€” refined across real projects until the output was something I could commit after a review pass rather than a rewrite. They work with GitHub Copilot, Claude, ChatGPT, JetBrains AI, or any capable LLM. Tested against Claude Sonnet 4 and GPT-4o, generating tests for JUnit 6.0 with Mockito 5.x and Spring Boot 3.4.

For background on JUnit 6 test structure, see the JUnit 6 Tutorial Series. For best practices on what makes a test maintainable, see Writing Your First Clean Test in JUnit 6.

Continue reading 10 AI Prompts to Generate JUnit 6 Tests for New Projects

JUnit 6 vs Spock vs TestNG: Best Testing Framework for Java?

If you are choosing a testing framework for a new Java project β€” or evaluating whether to migrate an existing one β€” you need an honest, detailed comparison of all three major options. JUnit 6, Spock, and TestNG each have genuine strengths and real trade-offs. This guide compares them across every dimension that matters: syntax, features, performance, ecosystem, and team fit.

Framework Profiles

Before diving into code, it helps to understand each framework at a high level. JUnit 6 is the direct evolution of the most widely used Java testing library in history, built on a clean three-module architecture. Spock takes a fundamentally different approach β€” it is written in Groovy and embraces a specification-style, BDD-inspired syntax that many developers find more expressive than annotation-driven frameworks. TestNG was created to address limitations in early JUnit versions and remains popular for its advanced grouping, parallel execution, and test dependency features. The table below captures the most important dimensions at a glance before we explore each in depth.

JUnit 6SpockTestNG
LanguageJavaGroovy (runs on JVM)Java
First release1997 (JUnit 1)20082004
ParadigmAnnotation-drivenSpecification-based (BDD)Annotation-driven
MockingSeparate (Mockito)Built-in (Spock Mocks)Separate (Mockito)
Spring Boot supportNative, first-classVia spock-springManual config
Learning curveLow (Java devs)Medium (need Groovy)Low (Java devs)

Syntax Comparison: The Same Test in All Three

The most revealing comparison is seeing exactly the same test case written in each framework side by side. All three examples below test the same behaviour: placing a valid order should save it via the repository and return an order with CONFIRMED status. Pay attention to how each framework handles the test structure, mock setup, and verification β€” the differences in verbosity and readability are immediately apparent and reflect each framework’s core design philosophy.

Continue reading JUnit 6 vs Spock vs TestNG: Best Testing Framework for Java?

JUnit 6 vs Mockito: Roles, Differences, and Integration

If you are new to Java testing, you might wonder: are JUnit 6 and Mockito competitors? Do I need both? When does Mockito replace JUnit 6? The answer is that they are not alternatives β€” they are complementary tools with completely different jobs. JUnit 6 runs your tests. Mockito creates the fake objects your tests use. This guide clarifies their distinct roles, shows how they integrate, and answers every question developers have about which tool does what.

What JUnit 6 Does

JUnit 6 is a test runner and test framework. Its responsibilities are:

  • Discovering test classes and test methods on the classpath
  • Managing the test lifecycle (@BeforeEach, @AfterEach, etc.)
  • Executing test methods and reporting pass/fail/skip
  • Providing assertion methods (assertEquals, assertThrows, assertAll)
  • Supporting parameterized tests, dynamic tests, and parallel execution
  • Integrating with build tools (Maven, Gradle) and IDEs

What Mockito Does

Mockito is a mocking framework. Its responsibilities are:

  • Creating fake (mock) implementations of interfaces and classes
  • Defining return values for method calls on mocks (stubbing)
  • Verifying that methods were called with expected arguments
  • Creating spies that wrap real objects with selective overrides
  • Capturing method arguments for detailed inspection

Mockito does NOT run tests. JUnit 6 does NOT create mocks. They work together.

The Division of Labour

// This test uses BOTH JUnit 6 and Mockito β€” each doing its own job

import org.junit.jupiter.api.*;          // JUnit 6: @Test, @BeforeEach, @DisplayName
import org.mockito.*;                    // Mockito: @Mock, @InjectMocks
import org.mockito.junit.jupiter.*;      // Bridge: connects the two
import static org.junit.jupiter.api.Assertions.*; // JUnit 6: assertions
import static org.mockito.Mockito.*;     // Mockito: when(), verify()

// JUnit 6 discovers and runs this class
@ExtendWith(MockitoExtension.class)  // Mockito registers as a JUnit 6 extension
class OrderServiceTest {

    // ---- Mockito's job: create fake dependencies ----
    @Mock
    private OrderRepository orderRepository; // fake repository

    @Mock
    private EmailService emailService;       // fake email service

    @InjectMocks
    private OrderService orderService;       // real class under test

    // ---- JUnit 6's job: lifecycle management ----
    @BeforeEach
    void printTestStart() {
        System.out.println("Test starting...");
    }

    // ---- JUnit 6's job: discover and execute this test ----
    @Test
    @DisplayName("Placing an order persists it and sends a confirmation email")
    void placingOrderPersistsItAndSendsEmail() {

        // ---- Mockito's job: define stub behaviour ----
        Order savedOrder = new Order(1L, "[email protected]", 99.99, OrderStatus.CONFIRMED);
        when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);

        // ---- The actual call under test ----
        Order result = orderService.placeOrder("[email protected]", 99.99);

        // ---- JUnit 6's job: assert the result ----
        assertNotNull(result.getId(),              "Order ID must be assigned");
        assertEquals(OrderStatus.CONFIRMED,        result.getStatus());

        // ---- Mockito's job: verify interactions ----
        verify(orderRepository, times(1)).save(any(Order.class));
        verify(emailService, times(1)).sendConfirmation("[email protected]");
    }
}
Continue reading JUnit 6 vs Mockito: Roles, Differences, and Integration