Next-Gen App & Browser Testing Cloud
Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles

To do JUnit testing in Java, add the JUnit 5 org.junit.jupiter:junit-jupiter dependency (with the Maven Surefire plugin) to your project, create a test class under src/test/java, write methods annotated with @Test, and verify behavior with assertions such as assertEquals and assertThrows. You then run the tests from your IDE or with mvn test. The rest of this guide walks through each step with working JUnit 5 code.
JUnit is the de facto standard framework for unit testing in Java. It lets you write small, repeatable, automated tests that check a single unit of code, typically one method, and report a clear pass or fail so regressions surface the moment you break something.
JUnit 5 (the current major version) is made up of three modules, and it helps to know which one you are using:
For new projects you write tests against Jupiter and only pull in Vintage if you still have old JUnit 4 tests to keep alive.
You need a JDK (Java 8 or later, 17 is a safe modern choice), a build tool, and the JUnit dependency. With Maven, the setup is three steps:
A minimal pom.xml fragment looks like this:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.1</version>
</plugin>
</plugins>
</build>If you use Gradle instead of Maven, the equivalent is a one-line dependency plus telling the test task to use the platform:
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
}
test {
useJUnitPlatform()
}A JUnit test is just a class with methods annotated with @Test. Each test method exercises some behavior and uses an assertion to state what the result should be. Suppose you have this simple class to test:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int divide(int a, int b) {
return a / b;
}
}The matching test class lives in src/test/java. Note the imports come from org.junit.jupiter.api, not the old org.junit package, and that test methods do not need to be public in JUnit 5:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorTest {
@Test
void addsTwoNumbers() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result, "2 + 3 should equal 5");
}
}When you run it, JUnit creates a fresh instance of the class, calls addsTwoNumbers(), and reports a pass because the actual value matches the expected one. The optional last argument is a failure message that shows up when the assertion fails.
Most tests need some preparation, like creating the object under test or opening a connection, and cleanup afterwards. JUnit 5 gives you four lifecycle hooks:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorLifecycleTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@Test
void addsPositiveNumbers() {
assertEquals(7, calculator.add(3, 4));
}
}Assertions live in org.junit.jupiter.api.Assertions and are usually static-imported. The ones you will reach for most are:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CalculatorAssertionsTest {
private final Calculator calculator = new Calculator();
@Test
void groupsRelatedChecks() {
assertAll("addition",
() -> assertEquals(4, calculator.add(2, 2)),
() -> assertEquals(0, calculator.add(-2, 2))
);
}
@Test
void throwsOnDivideByZero() {
ArithmeticException thrown = assertThrows(
ArithmeticException.class,
() -> calculator.divide(10, 0)
);
assertEquals("/ by zero", thrown.getMessage());
}
}Assertions are different from assumptions. A failed assertion fails the test, while a failed assumption (for example Assumptions.assumeTrue(condition)) aborts the test and reports it as skipped. Use assumptions when a test only makes sense under a precondition, such as a particular operating system or CI environment variable.
Beyond @Test and the lifecycle hooks, Jupiter ships several annotations that make tests more expressive and reduce duplication:
| Annotation | What it does |
|---|---|
| @DisplayName | Gives a test a human-readable name in reports, e.g. "returns sum of two positive numbers". |
| @Disabled | Skips a test or class, with an optional reason string for why. |
| @ParameterizedTest | Runs the same test many times with different inputs, fed by @ValueSource, @CsvSource, @MethodSource, and others. |
| @RepeatedTest(n) | Executes the same test n times, useful for flaky or timing-sensitive checks. |
| @Nested | Groups related tests in an inner class with its own lifecycle, mirroring the structure of the code under test. |
| @Tag | Categorizes tests (e.g. "fast", "slow") so you can include or exclude groups when you run them. |
A parameterized test removes copy-paste boilerplate. Here @CsvSource supplies pairs of inputs and the expected result:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorParameterizedTest {
private final Calculator calculator = new Calculator();
@DisplayName("adds pairs of numbers")
@ParameterizedTest
@CsvSource({"1, 1, 2", "2, 3, 5", "-1, 1, 0"})
void addsPairs(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}
}Once your tests are written, you can run them in several ways:
JUnit is also a popular runner for Selenium UI tests. The unit-testing structure stays identical, that is, the same @Test methods, lifecycle hooks, and assertions, but instead of a local driver you point a Selenium RemoteWebDriver at a cloud Selenium grid. With TestMu AI you can run those JUnit and Selenium tests in parallel across 3,000+ browser and operating system combinations, so your test suite gives you genuine cross-browser coverage without any local infrastructure. For a deeper walkthrough, see the JUnit Tutorial.
Use the JUnit 5 aggregator artifact org.junit.jupiter:junit-jupiter with test scope. It pulls in junit-jupiter-api (for writing tests), junit-jupiter-engine (for running them on the JUnit Platform), and junit-jupiter-params (for parameterized tests). Pair it with maven-surefire-plugin 3.x so mvn test picks the tests up.
JUnit 5 is split into three modules: the JUnit Platform (launches engines), JUnit Jupiter (the new annotations and assertions), and JUnit Vintage (runs legacy JUnit 3/4 tests). The annotations also changed: @Before/@After became @BeforeEach/@AfterEach, @BeforeClass/@AfterClass became @BeforeAll/@AfterAll, and assertions moved to org.junit.jupiter.api.Assertions with the message argument now last.
By default JUnit creates a fresh instance of the test class for every test method (PER_METHOD lifecycle), so a non-static @BeforeAll has no single instance to belong to. It must be static. If you annotate the class with @TestInstance(TestInstance.Lifecycle.PER_CLASS), JUnit reuses one instance and you can make @BeforeAll and @AfterAll non-static.
Use assertThrows. It takes the expected exception class and an executable lambda, runs the lambda, and fails the test if the expected exception is not thrown. It also returns the thrown exception, so you can make further assertions on its message, for example assertEquals("/ by zero", thrown.getMessage()).
An assertion that fails marks the test as failed. An assumption that fails (for example Assumptions.assumeTrue(condition)) aborts the test and reports it as skipped, not failed. Use assumptions to run a test only when a precondition holds, such as a specific operating system or CI environment variable.
Keep your JUnit test code the same and point your Selenium RemoteWebDriver at a cloud Selenium grid instead of a local driver. By passing different browser and operating system capabilities, the same @Test methods run in parallel across thousands of browser and OS combinations, giving you real cross-browser coverage without local setup.
KaneAI - Testing Assistant
World’s first AI-Native E2E testing agent.

TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance