JUnit: what it is, key concepts and how it works

Author: IT Sectr Published: 2026-04-08 Reading time: 8 min

JUnit is a standard framework for unit testing in the Java and Kotlin ecosystem, used in Android development to verify business logic at the level of isolated components. The framework provides a set of annotations, assertion methods, and a Test Runner for automatic discovery and execution of tests. According to JUnit.org, the library remains the most popular solution for unit testing in the JVM ecosystem: more than 70% of Java projects use JUnit in any version.

Key Takeaways

  • JUnit is an open-source framework for unit testing Java and Kotlin code.
  • Annotations — @Test, @BeforeEach, @BeforeAll manage the test class lifecycle.
  • Assertions — assertEquals, assertTrue, assertThrows verify expected execution results.
  • Parameterized tests — allow running one test with different sets of input data.
  • Test Runner — JUnit infrastructure automatically finds and runs all tests in the project.

What is JUnit?

JUnit is an open-source framework for writing and running repeatable unit tests in Java and Kotlin. It is part of the xUnit ecosystem — a family of frameworks based on the architecture by Kent Beck and Erich Gamma — and is the standard testing tool in Android Studio and IntelliJ IDEA.

The main task of JUnit is to isolate a small piece of code (method, class) and verify its behavior under controlled conditions. Tests are written as regular Java/Kotlin classes with annotations, and the framework handles test discovery, lifecycle management, and execution statistics collection.

The first version of JUnit appeared in 1997 and radically changed the development approach by popularizing Test-Driven Development (TDD) practices. Today, JUnit 5 (Jupiter) is the current version, completely redesigned with a modular architecture supporting Java 8+ and extensions.

How JUnit Works

The mechanism of JUnit execution is based on the Test Runner pattern, which scans the classpath, finds methods with the @Test annotation, creates a test class instance, and runs methods in a specific order. The Test Runner manages the lifecycle: BeforeAll → BeforeEach → Test → AfterEach → AfterAll.

Test Class Lifecycle

JUnit wraps each test method in a separate class instance, ensuring isolation between tests. This means that class fields do not retain state between different @Test methods — each test starts with a clean object.

Basic Test Structure

The simplest JUnit test is a method with the @Test annotation that calls the code under test and verifies the result using an assertion. If the assertion fails, the test is considered failed, and JUnit reports the error.

java
@Test
void additionShouldReturnCorrectSum() {
    var calculator = new Calculator();
    var result = calculator.add(2, 3);
    assertEquals(5, result);
}

The Test Runner automatically finds such a method, runs it, and reports the result. If the method does not throw an exception — the test passes (green). If an assertion fails — the test fails (red).

JUnit Annotations: Test Lifecycle

Annotations in JUnit define when and how test code should execute. In JUnit 5, annotations are located in the org.junit.jupiter.api package and cover all stages: data preparation, test execution, resource cleanup.

AnnotationPurposeExecutes
@TestMarks a test methodOnce per call
@BeforeEachSetup before each testBefore each @Test
@BeforeAllOne-time class initializationOnce before all tests
@AfterEachCleanup after each testAfter each @Test
@AfterAllOne-time class teardownOnce after all tests
@DisplayNameHuman-readable test nameDecoration

Example of Using Annotations

Consider a complete test class with a proper lifecycle. The setUp method creates a fresh Calculator instance before each test, and tearDown releases resources — for example, closing file descriptors or database connections.

java
class CalculatorTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    void subtractionShouldReturnCorrectResult() {
        int result = calculator.subtract(10, 4);
        assertEquals(6, result);
    }

    @AfterEach
    void tearDown() {
        calculator.reset();
    }
}

Assertions: Verifying Results in JUnit

Assertions are static methods that compare the actual result with the expected one and throw an exception if they do not match. JUnit 5 provides the org.junit.jupiter.api.Assertions class with over 25 methods for various verification scenarios — from simple value comparison to timeout checks and grouped assertions.

  • assertEquals(expected, actual) — checks equality via equals().
  • assertTrue(condition) — checks that a condition is true.
  • assertFalse(condition) — checks that a condition is false.
  • assertNotNull(object) — checks that an object is not null.
  • assertNull(object) — checks that an object is null.
  • assertThrows(exception, executable) — checks that code throws a specified exception.
  • assertAll(executables) — groups multiple assertions and executes all without stopping at the first failure.

assertThrows for Exception Testing

Special attention deserves the assertThrows method, which allows verifying that code correctly handles error situations — division by zero, null passing, limit exceeding.

java
@Test
void divisionByZeroShouldThrowException() {
    Calculator calc = new Calculator();

    ArithmeticException exception = assertThrows(
        ArithmeticException.class,
        () -> calc.divide(10, 0)
    );

    assertEquals("Cannot divide by zero", exception.getMessage());
}

Parameterized Tests in JUnit

Parameterized tests allow running the same test method with different sets of arguments. In JUnit 5, this is done using the @ParameterizedTest annotation combined with a data source — @ValueSource, @CsvSource, @MethodSource, or @EnumSource.

This approach radically reduces code duplication: instead of ten identical tests for different values, you write one parameterized method. According to the Google Testing Blog, parameterization reduces test code volume by 40–60% without losing coverage.

Example with CsvSource

The @CsvSource annotation passes multiple comma-separated rows to the test. Each row corresponds to one test run. JUnit automatically converts string values to the required types: int, long, String, and others.

java
@ParameterizedTest
@CsvSource({
    "1,    1,   2",
    "2,    3,   5",
    "10,  20,  30",
    "-1,   1,   0"
})
void additionWithMultipleInputs(int a, int b, int expected) {
    assertEquals(expected, a + b);
}

MethodSource for Complex Objects

When input data is more complex than simple numbers or strings, @MethodSource is used. It references a static method that returns a Stream of arguments — for example, a Stream of Arguments or a Stream of User objects for testing the DAO layer.

Integrating JUnit with Android Projects

In Android development, JUnit is used alongside AndroidX Test, which provides extensions for testing Activity, Content Provider, and other Android components. JUnit handles unit tests executed on the JVM without an emulator, while AndroidX Test handles instrumented tests on the device.

Gradle Setup

To add JUnit 5 to an Android project, simply include the org.junit.jupiter:junit-jupiter dependency in the module-level build.gradle. The AGP Gradle plugin supports running JUnit 5 tests on the JVM through the standard Test Runner.

kotlin
// build.gradle.kts
android {
    testOptions {
        unitTests.isIncludeAndroidResources = true
    }
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
    testImplementation("androidx.test:core-ktx:1.6.1")
}

Testing ViewModel with JUnit

In practice, JUnit is most often used for testing ViewModel and repositories — layers that do not require an Android context or emulator. Such a test runs in milliseconds and can be executed hundreds of times without wasting time.

  • ViewModel — state verification: loading, success, error.
  • Repository — data mapping and error handling verification.
  • UseCase — business logic verification with dependency mocks.

JUnit 4 vs JUnit 5: Key Differences

JUnit 5 (Jupiter) is not just a new version, but a completely redesigned platform divided into three modules: JUnit Platform (test execution on JVM), JUnit Jupiter (API for writing tests), and JUnit Vintage (backward compatibility with JUnit 4). This modularity allows connecting different Test Engines — for example, Spek for Kotlin or TestNG.

FeatureJUnit 4JUnit 5
Packageorg.junitorg.junit.jupiter
Base Annotation@Test (from junit.framework)@Test (from org.junit.jupiter.api)
Before/After@Before, @After, @BeforeClass@BeforeEach, @AfterEach, @BeforeAll
Parameterized@RunWith(Parameterized.class)@ParameterizedTest + @ValueSource
Extension@Rule, @ClassRule@ExtendWith, more flexible API
Java minJava 5Java 8+

Migrating from JUnit 4 to JUnit 5 does not require rewriting all tests — just add JUnit Vintage Engine and old tests will continue to work. New tests are recommended to be written with JUnit 5 to take advantage of extensions, lambda assertions, and built-in parameterization support.

Frequently Asked Questions

How is JUnit different from Mockito?

JUnit is a framework for writing and running tests, while Mockito is a library for creating mock objects. They are used together: JUnit manages test execution, and Mockito replaces dependencies of the class under test.

Can JUnit 5 be used in Android projects?

Yes, JUnit 5 is fully compatible with Android projects. For unit tests, simply add the junit-jupiter dependency to build.gradle. Instrumented tests still work through the AndroidX Test Runner.

What is the minimum set of annotations needed for a test?

At minimum, a single @Test annotation before the method is sufficient. @BeforeEach and @AfterEach are recommended for initialization and cleanup, but they are not required.

What is the Test Runner in JUnit?

The Test Runner is a JUnit component that scans the classpath, finds methods with the @Test annotation, creates test class instances, and runs the tests. In JUnit 5, this role is performed by the JUnit Platform with connected Test Engines.

How to verify that a method throws an exception?

Use assertThrows(Class, Executable) — it takes the expected exception type and a lambda with the method call. JUnit verifies that the exception was actually thrown and returns it for further inspection.

Summary

  • JUnit is the standard unit testing framework for Java and Kotlin, foundation of the xUnit ecosystem.
  • Annotations @Test, @BeforeEach, @AfterEach manage the test class lifecycle.
  • Assertions — assertEquals, assertTrue, assertThrows verify result correctness.
  • Parameterized tests with @CsvSource and @MethodSource reduce code duplication.
  • JUnit 5 — modular architecture with platform, Jupiter API, and extension support.
  • Android integration — JUnit runs on JVM through standard Test Runner without an emulator.
  • Test Runner automatically discovers and runs @Test methods, reporting results.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also