JUnit Tutorial: Learn JUnit 6 and JUnit 5 With Examples

JUnit is the testing framework for Java in which we mark methods with @Test, check results with assertions, and let a build tool or the IDE run every test and report which ones pass or fail. The current release is JUnit 6.1.3, which needs Java 17 or later. JUnit 6 keeps the JUnit 5 programming model and package names, raises the Java baseline and gives all modules one version number, as explained in JUnit 5 vs JUnit 6.

We use JUnit to test a class on its own, such as a price calculation or a validator, and to run the same checks on every build so that a change that breaks the behavior fails the build. Spring Boot, Mockito and Testcontainers all run their tests on top of JUnit.

The following example is a JUnit test for a shopping cart. It adds two items and checks the item count and the total in one test.

@Test
void itemCountSumsQuantities() {
    cart.add("apple", new BigDecimal("0.50"), 4);
    cart.add("bread", new BigDecimal("2.25"), 1);

    assertAll(
            () -> assertEquals(5, cart.itemCount()),                      // 4 apples + 1 bread
            () -> assertEquals(new BigDecimal("4.25"), cart.total()));   // 4 x 0.50 + 2.25
}
[INFO] Running com.howtodoinjava.junit.cart.ShoppingCartTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.105 s -- in com.howtodoinjava.junit.cart.ShoppingCartTest

Notice that the method is package-private and returns nothing, and that assertAll() reports both checks even if the first one fails. The examples in this JUnit tutorial run on JUnit 6.1.3 with Java 25, and most of them also compile on JUnit 5.x without changes. We start with the parts JUnit consists of, follow a reading path for beginners, and finish with an annotation table and a list of every JUnit tutorial on the site.

1. JUnit Architecture With the Platform, Jupiter and Vintage

JUnit 4 was one jar, and IDEs and build tools read its internal classes through reflection to find and run tests. A renamed private field inside JUnit could break Eclipse or Maven. JUnit 5 split the framework into separate parts with public APIs, and JUnit 6 keeps that split.

Maven Surefire, Gradle, the IDE and the Console Launcher call the JUnit Platform Launcher, which passes the work to the Jupiter, Vintage, Suite and third-party test engines, and each engine runs its own kind of tests
Build tools and IDEs talk only to the Launcher of the JUnit Platform, and each test engine runs its own kind of tests

JUnit consists of three projects, and each one has its own job.

  • The JUnit Platform launches testing frameworks on the JVM. Its Launcher discovers tests, filters them by class name or tag, runs them through a test engine and reports the results to Maven, Gradle or the IDE. It also defines the TestEngine API that every engine implements.
  • JUnit Jupiter is the programming model and the extension model we write tests with (@Test, @BeforeEach, Assertions, @ExtendWith), plus the Jupiter test engine that runs those tests.
  • JUnit Vintage is a test engine that runs JUnit 3 and JUnit 4 tests on the Platform. It needs JUnit 4.12 or later on the class path. In JUnit 6 the Vintage engine is deprecated and meant only for the time of a migration.

Because the build tool talks only to the Launcher, one mvn test run can execute Jupiter tests, old JUnit 4 tests and tests of other frameworks side by side. JUnit also ships the suite engine from junit-platform-suite, which runs @Suite classes, and other projects such as Cucumber plug their own engines into the same Platform.

In a Maven or Gradle build, these parts arrive as separate artifacts. Most projects need only the BOM and junit-jupiter.

ArtifactContainsWhen we add it
org.junit:junit-bomVersions of all JUnit artifactsAlways, imported once, so every JUnit module stays on 6.1.3
org.junit.jupiter:junit-jupiterJupiter API, parameterized tests and the Jupiter engineAlways, with test scope
org.junit.platform:junit-platform-launcherThe Launcher APIWhen an IDE or Gradle asks for it, or to run tests from Java code
org.junit.platform:junit-platform-suite@Suite annotations and the suite engineFor test suites
org.junit.vintage:junit-vintage-engineThe deprecated Vintage engineOnly while JUnit 4 tests still exist

1.1. Terms Used in JUnit Reports and Docs

The JUnit user guide and the IDE test views use a few terms with a precise meaning. Knowing them makes failure reports easier to read, because a report shows the tests as a tree of containers and tests. The definitions are the same in JUnit 5 and JUnit 6.

TermMeaning
ContainerA node in the test tree that contains other containers or tests, for example a test class
TestA node in the test tree that checks expected behavior when it runs, for example a @Test method
Lifecycle methodA method annotated or meta-annotated with @BeforeAll, @AfterAll, @BeforeEach or @AfterEach
Test classA top-level class, static member class or @Nested class with at least one test method. It must not be abstract and must have a single constructor. Java records work too
Test methodAn instance method annotated with @Test, @RepeatedTest, @ParameterizedTest, @TestFactory or @TestTemplate
Test engineAn implementation of TestEngine that discovers and runs one kind of tests, such as Jupiter or Vintage

Test classes, test methods and lifecycle methods do not have to be public, but they must not be private. Test methods and lifecycle methods must not return a value, except @TestFactory methods, which return the dynamic tests.

1.2. What JUnit 6 Changed in the Architecture

The three-part design is the same as in JUnit 5, but the JUnit 6.0 release changed a few things around it that show up in a build file.

  • All modules share one version number. JUnit 5 used Platform 1.x next to Jupiter 5.x, whereas JUnit 6 uses 6.x for everything.
  • JUnit needs Java 17 or later at runtime. JUnit 5 ran on Java 8.
  • The junit-platform-runner module is gone, so @RunWith(JUnitPlatform.class) no longer works and suites use @Suite.
  • Maven Surefire and Failsafe below 3.0.0 are not supported.
  • The Vintage engine is deprecated and reports an INFO-level discovery issue when it finds JUnit 4 test classes.
  • The JUnit API carries JSpecify nullability annotations, and @CsvSource parsing moved to the FastCSV library.

2. A Reading Path Through This JUnit Tutorial

A developer new to JUnit learns fastest by writing one passing test first, learning the build setup next, and only after that the annotations that control how tests run. The order below follows that idea. Each step links one tutorial with a runnable Maven project.

  1. Write and run the first tests in getting started with JUnit, including how to read a failed test report.
  2. Add JUnit to the build with the JUnit Maven dependency or the JUnit Gradle dependency, and learn to run one class or one method.
  3. Check results with the JUnit assertions, and test error cases with assertThrows().
  4. Learn the order in which JUnit calls the constructor, the lifecycle methods and the extensions in the JUnit test lifecycle.
  5. Pick the annotations a project needs from the table in section 3, for example parameterized tests, tags and timeouts.
  6. Measure which lines the tests cover with JaCoCo code coverage.
  7. Add mocks and real databases with the libraries in section 5.

Say a team writes its first tests for an order service. Steps 1 to 4 are enough for the first week, because they cover a working build, assertions and setup code. Parameterized tests and coverage limits pay off once the test suite grows past a few dozen classes.

3. JUnit Annotations and Their Tutorials

All Jupiter annotations live in org.junit.jupiter.api and its subpackages, in JUnit 5 and JUnit 6 alike, so the table applies to both versions. Each row names what the annotation does and links the tutorial that covers it with examples.

AnnotationWhat it doesTutorial
@TestMarks a method as a testThe reading path in section 2
@BeforeEachRuns before every test method of the class, for shared setupJUnit @BeforeEach: Run Setup Code Before Every Test
@AfterEachRuns after every test method, also when the test failsJUnit @AfterEach: Clean Up After Every Test (Examples)
@BeforeAll, @AfterAllRun once before and after all tests of the class; static by defaultJUnit @BeforeAll and @AfterAll: Run Setup Once per Class
@TestInstanceSets the test instance lifecycle; PER_CLASS allows non-static @BeforeAllNon-Static @BeforeAll and @AfterAll in JUnit (PER_CLASS)
@AutoCloseCloses the annotated field after the tests that use it (since JUnit 5.11)JUnit @AutoClose Annotation: Close Test Resources (Examples)
@ParameterizedTestRuns one method with several sets of arguments from sources such as @CsvSourceJUnit 5 Parameterized Tests: @ParameterizedTest With Examples
@RepeatedTestRuns a test a given number of timesJUnit 5 @RepeatedTest: Repeat Tests With RepetitionInfo
@TagLabels tests so that Maven, Gradle or a suite can include or exclude themJUnit 5 @Tag: Filter Tests With Tags and Tag Expressions
@DisabledSkips a test method or a whole test class, with an optional reasonJUnit 5 @Disabled: Skip a Test Method or a Whole Test Class
@EnabledOnOs, @EnabledIf and othersRun a test only when a condition holds, such as an OS or a system propertyJUnit 5 Conditional Test Execution: @EnabledOnOs, @EnabledIf
@TimeoutFails a test or lifecycle method that runs longer than the given durationJUnit 5 @Timeout Annotation: Time Limits for Slow Tests
@TempDirInjects a temporary directory and deletes it after the testJUnit 5 @TempDir: Temporary Directories and Files in Tests
@TestMethodOrder, @OrderSet the order in which test methods runJUnit 5 Test Execution Order With @Order and @TestMethodOrder
@Suite, @SelectPackagesGroup test classes into a suite run by the suite engineJUnit 5 Test Suites With @Suite: Select, Filter and Run

A few annotations from the annotation list do not have their own tutorial yet, but they appear in many test classes.

  • The annotation @DisplayName sets a readable name that IDEs and reports show instead of the method name.
  • The annotation @Nested groups related tests in a non-static inner class.
  • The annotation @ExtendWith registers an extension, such as the Mockito or the Spring extension.
  • The annotation @TestFactory marks a method that returns dynamic tests built at runtime.

We can also combine annotations into our own composed annotation. For example, an annotation @Fast that is itself annotated with @Tag(“fast”) and @Test marks a method as a test and tags it in one step.

4. All JUnit Tutorials by Topic

The tutorials in the reading path and in the annotation table cover the core of JUnit. The remaining tutorials go deeper into one area each, grouped by the question they answer.

4.1. Assertions, Assumptions and Architecture Tests

Assertions decide whether a test passes, and assumptions decide whether it runs at all. A failed assumption aborts the test, and the report counts it as skipped, not as passed.

TutorialWhat we learn there
JUnit 5 Assumptions: assumeTrue, assumeFalse, assumingThatHow assumeTrue(), assumeFalse() and assumingThat() skip tests when the environment is not ready
AssertJ Tutorial: Fluent Assertions Cheat Sheet for JUnit 6Fluent assertions with AssertJ as an alternative to the JUnit Assertions class
ArchUnit Tutorial: Test Java Architecture with JUnit 6Checking package dependencies and layering rules of a Java app with ArchUnit tests

4.2. Build Tools, IDEs and Test Reports

A CI server reads test results from report files, not from the console. Surefire writes XML reports by default, and an HTML report needs one extra plugin.

TutorialWhat we learn there
JUnit HTML Report With Maven Surefire Report and GradleCreating an HTML report from Surefire results with Maven or Gradle
JUnit XML Report: Surefire and Open Test Reporting FormatsThe legacy Surefire XML format and the Open Test Reporting XML format
Run JUnit Tests in Eclipse: Shortcuts, JUnit View, FixesRunning, debugging and creating tests in Eclipse IDE

4.3. JUnit Versions and Migration

Many projects still contain JUnit 4 tests. They can run next to Jupiter tests through the Vintage engine while the team migrates them class by class.

TutorialWhat we learn there
JUnit 5 vs JUnit 4: Differences and Migration GuideThe same test in both versions, an annotation mapping table and an OpenRewrite migration
JUnit 6 Nullability with JSpecify: @Nullable and @NullMarkedJSpecify @Nullable and @NullMarked in the JUnit 6 API and in our own code

4.4. Running Tests From Java Code and Listening to Results

Tools such as custom test runners or dashboards use the Platform APIs instead of a build tool. The Launcher API replaced the JUnit 4 class JUnitCore.

TutorialWhat we learn there
JUnitCore and Launcher API: Run JUnit Tests from Java CodeRunning JUnit 4 tests with JUnitCore and JUnit 6 tests with the Launcher API
JUnit Test Listener: TestExecutionListener and RunListenerReacting to test events with a TestExecutionListener, a TestWatcher or a JUnit 4 RunListener

4.5. JUnit in Spring Boot Apps

Spring Boot manages the JUnit version through its dependency management, so a Spring Boot project does not declare the JUnit version itself. Spring’s test support runs as a JUnit extension.

TutorialWhat we learn there
JUnit Version with Spring Boot 4: Check and Override ItWhich JUnit version Spring Boot 4.1.1 brings and how to override it
Test Spring Security Authentication with JUnit and MockMvcTesting login, roles, 401 and 403 responses with Spring Security test support

5. Related Testing Tutorials

JUnit runs the tests, but real projects add libraries for mocks, HTTP calls and databases. These libraries all run on the JUnit Platform, so the setup from this tutorial stays the same.

6. JUnit Tutorial FAQs

Readers who start with JUnit today mostly ask which version to learn and what happens to existing JUnit 4 and JUnit 5 tests.

6.1. Should We Learn JUnit 5 or JUnit 6?

JUnit 6, if the project runs on Java 17 or later. The test code is almost the same, because JUnit 6 keeps the org.junit.jupiter.api package and its annotations, so everything learned on one version applies to the other. Projects still on Java 8 or 11 stay on JUnit 5 until they upgrade the JDK.

6.2. Do JUnit 5 Tests Run on JUnit 6 Without Changes?

In most cases yes, after changing the BOM version to 6.1.3. Code fails to compile only where it uses an API removed in 6.0, such as MethodOrderer.Alphanumeric (use MethodOrderer.MethodName) or the @RunWith(JUnitPlatform.class) runner (use @Suite). The build also needs Maven Surefire 3.0.0 or later.

6.3. Can JUnit 4 and JUnit 6 Tests Run in the Same Project?

Yes. We add junit-vintage-engine next to junit-jupiter, and keep the junit:junit 4.13.2 dependency. The Platform runs both engines in one build. The Vintage engine is deprecated in JUnit 6, so we treat it as a bridge and move the JUnit 4 classes to Jupiter over time.

6.4. Is JUnit Only for Unit Tests?

No. JUnit runs any test written as a Java method. Spring Boot integration tests, Testcontainers database tests and REST API tests all run as JUnit tests. In a Maven build, we run unit tests with Surefire in the test phase and slower integration tests with Failsafe in the integration-test phase.

7. Conclusion

JUnit consists of the Platform, which IDEs and build tools call, the Jupiter API and engine, which we write tests with, and the deprecated Vintage engine for JUnit 4 tests. JUnit 6.1.3 is the current release and needs Java 17. The Jupiter API is the same as in JUnit 5, so most JUnit 5 tests need no change beyond the version number.

A beginner gets the most out of the reading path in section 2, from the first test to code coverage. After that, the annotation table and the topic groups point to a tutorial for each feature as the project needs it.

8. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. hi can you explain me how to use junit with spring i have many doubt in it
    for example 1) how to configure application context for testing .. and many more

  2. Thanks for writing this blog post, it’s short and to the point!

    As a test writer, you may not feel that much different but when you will go for its extension or try to develop any IDE plugin, you will praise it.

    I have to say that, looking into the JUnit5 source code, I think it’s a lot more complex than JUnit4, writing an extension for JUnit5 that changes how test methods are executed seems next to impossible to me (whereas in JUnit4 that’s trivial)!

    I want to be able to run every test method on a remote machine. In JUnit4, I would implement a Runner and be done in 5 minutes. In JUnit5, it seems I have to implement a full-blown TestEngine?! The Extension mechanism allows post processing a test instance, but not replacing it entirely (or at least overriding the methods).

    Any ideas on how to proceed other than just sticking with JUnit4?

Comments are closed.

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.