JUnit

JUnit provides JVM test execution infrastructure and, through Jupiter, a programming and extension model for writing tests. Start with a meaningful behavior assertion, make fixture ownership explicit, and verify that the same tests are discovered by your IDE and build pipeline.

A green build is useful evidence only when the intended tests actually ran. Dependency alignment, engine selection, naming filters, and lifecycle settings are part of a reliable suite rather than setup details to forget after the first test passes.

TL;DR

Quick Example

This self-contained Jupiter example tests a fictional capacity calculation. The build below targets JDK 17 and JUnit 6.1.3, the versioned documentation used here. In a disposable project with a compatible Gradle installation, create settings.gradle.kts containing rootProject.name = "junit-example", then save this as build.gradle.kts:

Create src/test/java/CapacityTest.java:

Run gradle test from that project, or the equivalent task through its checked-in Gradle wrapper. Expect five test invocations: three input rows and two ordinary tests. Inspect the generated test report. Then temporarily change one expected value, verify the task fails, and restore it.

The example checks arithmetic boundaries and fixture independence, not a real reservation system. It intentionally excludes persistence and concurrency. A production capacity guarantee needs a transaction and race-handling design beyond this pure function.

Core Concepts

Platform, Jupiter, and engines

The JUnit overview separates the Platform's launch and engine infrastructure from Jupiter's programming model. Vintage provides an engine for older JUnit tests and is deprecated as a long-term migration destination in this documentation.

This distinction helps diagnose “tests compile but do not execute.” A test annotation can be available to the compiler while the intended engine or build integration is missing. JUnit 6 requires Java 17 or newer at runtime; an older project must review its test runtime before copying this setup.

Assertions describe observable behavior

An assertion should explain what the caller relies on: a return value, an exception, a state change, or an external interaction with a meaningful contract. Prefer a clear expected value to a complicated expression that independently reimplements the production algorithm.

For exception behavior, assert the relevant exception type and, when contractually important, a stable property. Avoid binding tests to incidental punctuation or implementation stack traces. Likewise, a test that merely executes a method without checking its result may miss a regression unless successful execution is itself the behavior under test.

Parameterized tests express a case matrix

Parameterized tests run the same method with arguments from a source. CsvSource is useful for small input/expected-output tables; richer objects or setup may justify another argument source. Parameter support requires the appropriate params artifact, included through the aggregate Jupiter dependency in this example.

Select cases because they distinguish behavior: lower bound, typical input, upper bound, and invalid values. Hundreds of similar rows do not automatically provide stronger coverage than a small deliberate set. Keep each failure's input identifiable in the report.

Fixture Lifetime Is a Test Design Decision

Jupiter's default lifecycle uses a fresh test-class instance per method. BeforeEach prepares per-test state. A per-class lifecycle shares one instance, which can be useful but makes mutable instance state an explicit responsibility.

Neither lifecycle creates a clean universe around the test. Static caches, environment settings, files, a shared application server, and committed database rows can outlive the Java test object. Identify which fixture owns each resource and what resets it between invocations.

For a database integration test, a rollback on one connection cannot clean up committed work on a separate connection. For parallel execution, even a unique object instance can still target the same external record. Use isolated identifiers, schemas, or disposable dependencies appropriate to the contract.

Extensions and Build Integration

Jupiter extension registration supports mechanisms including ExtendWith and RegisterExtension. Use an extension when several tests need a coherent lifecycle or parameter-resolution capability. Document when the resource starts, how it is supplied to tests, and who closes it.

Avoid hiding essential behavior behind a project-wide annotation that starts half the application for every unit test. If only a handful of tests need a real database, make that boundary visible. Separate fast behavior tests from integration suites using a deliberate build convention, tags, or tasks.

The build-support guide documents Gradle's Platform integration and Maven's engine and plugin requirements. Check the selected tool's discovery filters and dependency graph. An IDE's bundled launcher can behave differently from a misconfigured command-line build, so success in one does not prove the other is configured.

Keep compiler, runtime, engine, and launcher versions aligned through the project's dependency management. When upgrading, run a representative mixed suite and compare discovered counts, skipped tests, and failures. Do not silently accept disappearing tests as a successful migration.

Best Practices

Prove discovery and failure propagation

Run a named test through the build tool and inspect its report, then make a controlled assertion fail in a disposable change. Verify the failing task reaches CI as failure instead of being ignored by a wrapper.

Keep fixtures smaller than the question

Test pure policy without booting unrelated infrastructure. Add real dependencies where their semantics matter, and label those tests so their cost and failure modes are understandable.

Diagnose before adding retries

Retain inputs, logs, and fixture state needed to distinguish a product race, a test coupling, and a runtime-startup problem. A passing retry does not explain the first failure or establish reliability.

Common Mistakes

Bad: Assume the test suite passed because compilation succeeded.

Correct: Verify discovery, actual invocation counts, test reports, and failure propagation through the selected runner.

Bad: Share mutable static state and rely on method order.

Correct: Give tests independent state or define an explicitly ordered scenario whose dependencies are intentional and visible.

Bad: Add a container extension to every unit test for consistency.

Correct: Use real dependencies for the behaviors that need them, keeping pure-function tests quick and focused.

FAQ

Is JUnit only for Java?

It runs on the JVM and Jupiter tests can also be written in Kotlin. Language syntax, lifecycle conventions, and build integration still need to match the project.

Is JUnit 6 a completely different test annotation model?

Jupiter remains the programming model used here. Review the selected release's runtime and build requirements rather than assuming an older configuration works unchanged.

Does BeforeEach reset the database?

Only if your setup explicitly performs that work correctly. The annotation schedules your method; it does not infer which external resources require isolation.

When should I use parameterized tests?

Use them when the same behavior and assertion structure applies to a clear set of inputs. Separate tests can be clearer when cases require substantially different setup or expected interactions.

Related Topics

References