Power Your Software Testing with AI Agents and Cloud
The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. Test Intelligently and Ship Faster.
- TestMu AI (Formerly LambdaTest)
- /
- Learning Hub
- /
- What Is JUnit? Framework Overview and Architecture
What Is JUnit? Framework Overview and Architecture
What JUnit is, how it discovers and runs tests, the Platform, Jupiter, and Vintage architecture, JUnit 6 features, advantages and limits, and a first test.
Published on:
Every Java build tool, IDE, and CI server assumes your tests are JUnit tests, so the first mvn test on a new project runs into JUnit's conventions whether you chose the framework or not. JUnit is an open-source unit testing framework for Java and the JVM: you mark a method with @Test, call the code under test, and state the expected result with an assertion; a launcher inside Maven, Gradle, or your IDE then discovers those methods, runs them, and reports which passed. This chapter explains what the framework is, how a test travels from discovery to report, how the Platform, Jupiter, and Vintage modules fit together, and where JUnit is strong and where it needs help.
TL;DR
JUnit is the standard testing framework for Java: annotations mark test methods, assertions decide pass or fail, and a launcher in Maven, Gradle, or the IDE runs and reports them. JUnit 6 is the current generation, requires Java 17, and keeps the JUnit 5 Jupiter API, so existing tests compile unchanged.
- JUnit Platform - the foundation that launches test engines on the JVM; build tools and IDEs talk to it, not to your tests directly.
- JUnit Jupiter - the programming and extension model you write tests against: @Test, @BeforeEach, @ParameterizedTest, assertions, and @ExtendWith.
- JUnit Vintage - a deprecated engine that runs JUnit 3 and 4 tests on the Platform while a suite migrates to Jupiter.
- One dependency - junit-jupiter 6.1.3 with test scope plus Surefire 3.6.0 is a complete setup; the Maven Dependency chapter has the full pom.xml.
- Scaling a suite - JUnit has no built-in distribution; TestMu AI HyperExecute runs an unchanged Maven project across parallel virtual machines and merges the Surefire reports.
What Is JUnit?
JUnit is a unit testing framework for Java that belongs to the xUnit family, the group of frameworks that share the test case, fixture, assertion, and runner model. It is the default choice in the Java ecosystem. In the JetBrains Developer Ecosystem Survey 2025, 62% of Java developers said they use JUnit for unit testing, against 31% for Mockito and under 5% for TestNG, and 27% said they write no unit tests at all. JetBrains did not chart that question in the 2025 report, so the figures are weighted shares computed from the raw dataset it publishes under CC BY 4.0.
The current generation is JUnit 6. According to junit.org, it requires Java 17 and Kotlin 2.1 or above and provides the foundation for developer-side testing on the JVM. JUnit 6 kept the JUnit 5 programming model, which the project calls Jupiter, so most of what this tutorial teaches applies to both versions.
A JUnit test is an ordinary Java method. The framework supplies these pieces around it:
- Annotations - @Test marks a test method, and @BeforeEach, @AfterEach, @BeforeAll, and @AfterAll run setup and teardown around it.
- Assertions - static methods such as assertEquals, assertTrue, assertThrows, and assertAll compare the observed result with the expected one and fail the test when they differ.
- A test engine - the Jupiter engine finds annotated methods, creates a test instance, runs the lifecycle, and records the outcome of every test.
- Reporting - results flow to the console and to XML files that Maven Surefire, Gradle, Jenkins, GitHub Actions, and GitLab read to show pass and fail counts.
JUnit in Java is used for unit tests first, where one class is exercised in isolation. Because every build tool and IDE already knows how to run it, the same runner also executes integration tests and browser tests: a Selenium test in Java is usually a JUnit or TestNG test method that drives a WebDriver. What JUnit does not do is mock dependencies, drive a browser, or describe behavior in plain language; Mockito, Selenium, and Cucumber sit beside it for those jobs.
How Does JUnit Work?
A JUnit run moves through discovery, execution, and reporting, and knowing the stages explains most of the errors you will meet later.
- Discovery - the build tool or IDE hands the JUnit Platform launcher a set of classes under src/test/java. The Jupiter engine scans them for @Test, @ParameterizedTest, @RepeatedTest, and @TestFactory methods and builds a test plan. Surefire only scans class names that match its include patterns, which is why a class named CalculatorSpec is silently skipped while CalculatorTest runs.
- Execution - the engine runs @BeforeAll once per class, then for each test creates a new instance of the test class and runs @BeforeEach, the test method, and @AfterEach, and finishes with @AfterAll. An assertion that fails throws an AssertionFailedError, and any uncaught exception marks the test as failed.
- Reporting - every start, pass, fail, and skip is published as an event. The console shows a summary, Surefire writes target/surefire-reports/TEST-*.xml, and the JUnit Platform can also emit the newer Open Test Reporting format that CI dashboards read.
The annotations and assertions are the two levers you use every day. The JUnit Annotations chapter lists every annotation and the order the lifecycle methods run in, and the JUnit Assertions chapter covers each assertion method with an example.
JUnit Architecture: Platform, Jupiter, and Vintage
Since JUnit 5 the framework is three modules, and JUnit 6 kept that layout while giving all three the same version number. The split is what lets one launcher run Jupiter tests, old JUnit 4 tests, and third-party engines in the same build.
| Module | What it does | Artifact you depend on |
|---|---|---|
| JUnit Platform | The foundation for launching testing frameworks on the JVM. It defines the TestEngine API that engines implement and the Launcher API that Maven Surefire, Gradle, and IDEs call to discover and run tests. | junit-platform-launcher, pulled in transitively; junit-platform-console-standalone for the command line |
| JUnit Jupiter | The programming model and extension model for writing tests: the annotations, assertions, parameterized tests, and @ExtendWith. It ships its own TestEngine so the Platform can run Jupiter tests. | junit-jupiter (aggregates junit-jupiter-api, junit-jupiter-engine, and junit-jupiter-params) |
| JUnit Vintage | A TestEngine that runs JUnit 3 and JUnit 4 tests on the Platform. The JUnit team has deprecated it and recommends using it only while migrating to Jupiter or another framework. | junit-vintage-engine plus the old junit 4.13.2 artifact |
All three modules are released together. The JUnit release notes record 6.0.0 on September 30, 2025, the release that set the Java 17 baseline and the single version number, and 6.1.3 on August 7, 2026, which is the version on Maven Central at the time of writing. The junit-bom artifact pins every module to one version so a project never mixes a 6.1 engine with a 6.0 API. The JUnit Jupiter chapter goes deeper into the module you write against, and the JUnit Maven Dependency chapter shows the pom.xml for each artifact.
JUnit Features
These are the JUnit features a working suite uses, each with the chapter that covers it in depth.
- Lifecycle annotations - @BeforeAll and @AfterAll run once per class, @BeforeEach and @AfterEach around every test, and @TestInstance switches to one instance per class when setup is expensive.
- Assertions and assumptions - assertEquals, assertThrows, assertAll, and assertTimeout decide the outcome; assumeTrue aborts a test instead of failing it when a precondition such as an environment variable is missing.
- Parameterized tests - @ParameterizedTest with @ValueSource, @CsvSource, @CsvFileSource, @MethodSource, or @EnumSource runs one method against many inputs. See JUnit Parameterized Tests.
- Repeated and dynamic tests - @RepeatedTest runs a method a fixed number of times, and @TestFactory generates tests at runtime from a stream of data.
- Nested classes and display names - @Nested groups related tests inside an outer class and @DisplayName gives each a readable label in reports. See JUnit Nested Tests.
- Tags and conditional execution - @Tag("smoke") lets Maven run a subset, and @Disabled, @EnabledOnOs, and @EnabledIfEnvironmentVariable switch tests off by condition.
- Extension model - @ExtendWith registers extensions that hook into the lifecycle, resolve parameters, and handle exceptions; MockitoExtension and SpringExtension are built on it. See JUnit Extensions.
- Parallel execution - a junit-platform.properties file turns on concurrent classes or methods without changing a test. See JUnit Parallel Testing.
- Suites and reports - @Suite with @SelectPackages composes suites in code, and junit-platform-reporting writes legacy XML or Open Test Reporting XML for CI servers.
Advantages of JUnit
Teams pick JUnit for reasons that have less to do with any single feature than with how much of the Java toolchain assumes it.
- Zero-configuration tooling - IntelliJ IDEA, Eclipse, Maven Surefire, and Gradle discover and run JUnit tests out of the box, VS Code does the same through its Java extension pack, and every CI server reads the Surefire XML they produce.
- Stable API across generations - a Jupiter test written for JUnit 5 compiles unchanged on JUnit 6, and Vintage still runs a JUnit 4 suite during the migration, so upgrading rarely means rewriting.
- Ecosystem support - Mockito publishes mockito-junit-jupiter (5.24.0 on Maven Central) so mocks plug in through @ExtendWith, and Testcontainers and Spring ship JUnit 5 extensions of their own. The Mockito with JUnit chapter shows the pairing.
- Readable failures - assertion messages, @DisplayName labels, and assertAll grouping report exactly which expectation failed, which shortens the time from red build to fix.
- Built-in parallelism and filtering - concurrent execution and @Tag filtering are configuration, not code, so a suite can run its smoke tests on every commit and the full set on a schedule.
- Small dependency footprint - one test-scoped artifact and a Surefire plugin are the whole setup; there is no XML suite file to maintain unless you want one.
Limitations of JUnit
JUnit is a runner and an assertion library, and a few gaps show up as soon as a project moves past unit tests.
- No mocking of its own - stubbing a repository or an HTTP client needs Mockito or a similar library; JUnit only runs and asserts.
- No built-in retry - a flaky browser test cannot be re-run by an annotation; you write an extension or rely on the build tool, whereas TestNG ships IRetryAnalyzer.
- No test dependencies by design - JUnit has no equivalent of TestNG's dependsOnMethods, because each test is meant to run alone and in any order. That is a strength for unit tests and an adjustment for end-to-end flows that were written as a sequence.
- Suite composition lives in code - there is no testng.xml; @Suite, @SelectPackages, and @Tag replace it, so changing what runs means recompiling or passing a Surefire property.
- Plain reports - Surefire XML is machine-readable, and the HTML report needs the Surefire report plugin or a CI plugin; screenshots and step logs come from Allure, ExtentReports, or the test platform, not from JUnit.
- Migration cost from JUnit 4 - @Before became @BeforeEach, @Ignore became @Disabled, @RunWith became @ExtendWith, and Vintage is deprecated, so a large JUnit 4 suite has real work ahead of it. The JUnit 4 vs JUnit 5 chapter maps the annotations and JUnit 6 Migration covers the move to the Java 17 baseline.
If suite XML, dependencies, and retries matter more to your team than IDE and Spring defaults, the JUnit vs TestNG chapter compares the two frameworks feature by feature.
Your First JUnit Test
A Maven project needs one dependency and two plugins. The junit-bom import pins every JUnit module to 6.1.3, Surefire 3.6.0 knows how to talk to the JUnit Platform, and a pinned compiler plugin makes sure the sources compile for Java 17. That last plugin matters: when I first ran this project on a fresh Linux VM, the build died with Source option 5 is no longer supported, because the machine's default maven-compiler-plugin 3.1 ignores the release property and targets Java 5.
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>The class under test is a two-method Calculator whose divide() throws an IllegalArgumentException for a zero divisor. The test class lives in src/test/java, creates a fresh Calculator before every test, and checks a value, a quotient, and the exception:
package demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@Test
@DisplayName("add() returns the sum of two numbers")
void addsTwoNumbers() {
assertEquals(5, calculator.add(2, 3), "2 + 3 should be 5");
}
@Test
@DisplayName("divide() returns the integer quotient")
void dividesTwoNumbers() {
assertEquals(4, calculator.divide(12, 3));
}
@Test
@DisplayName("divide() rejects a zero divisor")
void rejectsZeroDivisor() {
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> calculator.divide(1, 0));
assertEquals("divisor must not be zero", error.getMessage());
}
}Run it with mvn test. Maven compiles the sources, then Surefire hands CalculatorTest to the JUnit Platform and prints one line per class with the counts of tests run, failed, and skipped. This is the output from the run I made on a TestMu AI HyperExecute Linux VM (job b2ee1c5a, Java 17.0.16, Maven with Surefire 3.6.0), trimmed to the lines that matter:
[INFO] --- maven-compiler-plugin:3.13.0:compile (default-compile) @ junit-demo ---
[INFO] Compiling 1 source file with javac [debug release 17] to target/classes
[INFO] --- maven-compiler-plugin:3.13.0:testCompile (default-testCompile) @ junit-demo ---
[INFO] Compiling 1 source file with javac [debug release 17] to target/test-classes
[INFO] --- maven-surefire-plugin:3.6.0:test (default-test) @ junit-demo ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running demo.CalculatorTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.085 s -- in demo.CalculatorTest
[INFO] Results:
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.125 sThe lines that matter are the provider and the count. Using auto detected provider JUnitPlatformProvider confirms Surefire found the JUnit Platform rather than falling back to the JUnit 4 provider, which is the usual reason a Jupiter suite reports zero tests. Tests run: 3 with a time of 0.085 s is the whole cost of a unit test: the 5 seconds around it are compilation and plugin startup, which is why splitting a suite across machines only pays once there are hundreds of classes. The JUnit Setup chapter covers the JDK, JAVA_HOME, and the IDE route, and JUnit Test Cases shows how to structure and name tests once you have more than one class.
Note: Run the same JUnit project across 3,000+ browser and OS combinations on TestMu AI when the tests drive a browser. Try TestMu AI free!
Best Practices for JUnit
These habits keep a JUnit suite fast to run and cheap to change.
- Keep tests independent - every test builds its own state in @BeforeEach and never relies on another test having run, which is what makes @TestMethodOrder unnecessary and parallel execution safe.
- Test one behavior per method - name the method after the behavior and add @DisplayName when the sentence needs spaces, so a failure reads as a requirement, not a method name.
- Group related checks with assertAll - a test that verifies three fields of one object should report all three failures at once instead of stopping at the first.
- Prefer @ParameterizedTest to copies - five inputs are five rows in a @CsvSource, not five near-identical methods.
- Tag by intent - @Tag("smoke") and @Tag("integration") let the pipeline run the fast set on every push and the slow set on a schedule from the same codebase.
- Treat Vintage as temporary - plan the JUnit 4 migration rather than adding new tests to a deprecated engine.
One practice for suites that have outgrown a single machine: JUnit distributes nothing itself. HyperExecute takes the same Maven project and a hyperexecute.yaml, discovers the test classes, splits them across fresh virtual machines with Auto-Split, runs each slice with mvn test -Dtest=$test, caches the .m2 directory against the pom.xml checksum, retries only commands that exit non-zero, and merges the Surefire reports into one dashboard. TestMu AI positions that as up to 70% faster than a hub-and-node grid, because the test code, the dependencies, and the runtime sit on one machine with no network hop between them. The HyperExecute getting started guide has the YAML for a Maven project.
Conclusion
Add the junit-jupiter 6.1.3 dependency and Surefire 3.6.0 to a Maven project, write one @Test method with one assertion, and run mvn test; everything else in JUnit is a refinement of that loop. The framework earns its place as the Java default by staying small at the center and letting build tools, IDEs, Mockito, and the extension model do the rest.
The next chapter, JUnit Setup, installs the JDK and gets the first test running in IntelliJ IDEA, and the JUnit Maven Dependency chapter explains every artifact in the pom.xml. When you want to prove the skill, the JUnit certification tests the same material.
Author
Himanshu Sheth is the Director of Marketing (Technical Content) at TestMu AI, with over 8 years of hands-on experience in Selenium, Cypress, and other test automation frameworks. He has authored more than 130 technical blogs for TestMu AI, covering software testing, automation strategy, and CI/CD. At TestMu AI, he leads the technical content efforts across blogs, YouTube, and social media, while closely collaborating with contributors to enhance content quality and product feedback loops. He has done his graduation with a B.E. in Computer Engineering from Mumbai University. Before TestMu AI, Himanshu led engineering teams in embedded software domains at companies like Samsung Research, Motorola, and NXP Semiconductors. He is a core member of DZone and has been a speaker at several unconferences focused on technical writing and software quality.
Reviewer
Srinivasan Sekar is Director of Engineering at TestMu AI (formerly LambdaTest), where he leads engineering and open-source initiatives behind the Selenium and Appium automation grid and owns TestMu AI's MCP Server. A committer to Appium and a contributor to Selenium, WebdriverIO, Taiko, and AppiumTestDistribution, he brings over 15 years of experience in quality engineering and open-source technologies. He is the author of the Apress book 'The MCP Standard: A Developer's Guide to Building Universal AI Tools with the Model Context Protocol,' a Certified Kubernetes and Cloud Native Associate, and an international conference speaker. Before TestMu AI he spent over eight years at Thoughtworks as a Principal Consultant and Quality Architect. Srinivasan holds a B.Tech in Information Technology from Anna University.
What Is JUnit FAQs
Did you find this page helpful?
More Related Learning Hubs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance
- Advanced access controls
- Advanced data retention rules
- Advanced Local Testing
- Premium Support options
- Early access to beta features
- Private Slack Channel
- Unlimited Manual Accessibility DevTools Tests



