Hero Background

Next-Gen App & Browser Testing Cloud

Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles

Next-Gen App & Browser Testing Cloud

How to do JUnit testing in Java?

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.

What JUnit Is and How JUnit 5 Is Structured

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:

  • JUnit Platform: The foundation that launches test engines. Build tools, IDEs, and the standalone console launcher all run on top of it.
  • JUnit Jupiter: The modern programming and extension model, that is, the org.junit.jupiter.* annotations and assertions you write tests with, plus the engine that runs them.
  • JUnit Vintage: A compatibility engine that runs legacy JUnit 3 and JUnit 4 tests on the JUnit Platform, so you can migrate gradually.

For new projects you write tests against Jupiter and only pull in Vintage if you still have old JUnit 4 tests to keep alive.

Step 1 - Set Up JUnit 5 in a Maven Project

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:

  • Add the org.junit.jupiter:junit-jupiter aggregator dependency with <scope>test</scope>. It transitively brings in the API, the engine, and the parameterized-test support.
  • Add (or update) the maven-surefire-plugin to version 3.x so Maven discovers and runs Jupiter tests on the JUnit Platform.
  • Put your production code in src/main/java and your tests in src/test/java, using class names like CalculatorTest.

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()
}

Step 2 - Write Your First JUnit Test Class

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.

Step 3 - Manage Setup with Lifecycle Annotations

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:

  • @BeforeEach: Runs before every test method, ideal for building a clean object under test each time.
  • @AfterEach: Runs after every test method, used to release resources opened in the test.
  • @BeforeAll: Runs once before all tests in the class. The method must be static (unless the class uses @TestInstance(PER_CLASS)).
  • @AfterAll: Runs once after all tests complete, following the same static rule, good for tearing down something expensive shared across tests.
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));
    }
}

Step 4 - Verify Behavior with Assertions

Assertions live in org.junit.jupiter.api.Assertions and are usually static-imported. The ones you will reach for most are:

  • assertEquals(expected, actual): Passes when the two values are equal; the optional message argument is last in JUnit 5.
  • assertTrue / assertFalse: Checks a boolean condition.
  • assertNull / assertNotNull: Asserts whether a reference is null.
  • assertThrows: Asserts that a piece of code throws a given exception, and returns the exception so you can inspect it further.
  • assertAll: Groups several assertions and reports every failure at once instead of stopping at the first.
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.

Step 5 - Use Jupiter's Productivity Annotations

Beyond @Test and the lifecycle hooks, Jupiter ships several annotations that make tests more expressive and reduce duplication:

AnnotationWhat it does
@DisplayNameGives a test a human-readable name in reports, e.g. "returns sum of two positive numbers".
@DisabledSkips a test or class, with an optional reason string for why.
@ParameterizedTestRuns 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.
@NestedGroups related tests in an inner class with its own lifecycle, mirroring the structure of the code under test.
@TagCategorizes 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));
    }
}

Step 6 - Run Your Tests

Once your tests are written, you can run them in several ways:

  • From your IDE: In IntelliJ IDEA or Eclipse, click the run gutter icon next to a test method or class to run it and see the green or red result inline.
  • With Maven: Run mvn test (or mvn clean test) from the project root. To run only tagged tests, use mvn test -Dgroups="fast".
  • With Gradle: Run gradle test, which executes the tests via the JUnit Platform you configured earlier.
  • From the console launcher: Use the JUnit Platform standalone console launcher jar to run tests in CI without a full build tool.

Run JUnit and Selenium Tests at Scale

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.

Frequently Asked Questions

Which JUnit dependency should I use for a new project?

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.

What is the difference between JUnit 4 and JUnit 5?

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.

Why does @BeforeAll have to be static?

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.

How do I test that a method throws an exception?

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()).

What is the difference between assertions and assumptions in JUnit?

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.

How do I run JUnit tests across multiple browsers?

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.

Related Questions

Test Your Website on 3000+ Browsers

Get 100 minutes of automation test minutes FREE!!

Test Now...

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

  • 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