Which Is Best for Java Unit Testing

Which Is Best for Java Unit Testing JUnit 4, JUnit 5, or TestNG?

In modern software development, writing code is only half the battle — testing that code is just as critical. Unit testing helps developers verify that individual units of logic (like methods or classes) behave as expected.By writing unit tests, we not only catch bugs early in the development process, but also make the codebase easier to maintain, refactor, and scale over time.

When it comes to unit testing in Java, the most widely used frameworks are:
JUnit 4
JUnit 5
TestNG

Today, most companies prefer using JUnit 5 or TestNG because of their modern features and flexibility. However, JUnit 4 is still important to learn — especially when working with legacy projects or teams maintaining older codebases.

What is Unit Testing, Really?

Unit testing means that developers write tests to check the behavior of individual methods or components — in isolation — before handing the code over to testers or deploying it further.

A common misconception is that only QA/testers write tests. In reality, developers write unit tests for the methods they develop. These tests ensure that the code performs correctly under different conditions (inputs, outputs, exceptions, etc.)
Common Java Unit Testing Frameworks

When writing unit tests in Java, the three most commonly used frameworks are:

  • JUnit 4 – A long-standing framework with a simple structure.
  • JUnit 5 – A modern, modular rewrite of JUnit 4.
  • TestNG – Inspired by JUnit but built with more advanced testing features.

Let’s explore each one and compare their syntax, usage, and key differences.

JUnit 4 – The Classic Framework


🔹 Key Features:

    • Simple and widely used.
    • Annotations like @Test, @Before, @After, @BeforeClass, @AfterClass.
    • No support for modern Java features like lambdas or streams.
    • Still used in many legacy codebases.

    ✅ Example:

    import org.junit.Test;
    import static org.junit.Assert.*;
    
    public class CalculatorTest {
    
        @Test
        public void testAddition() {
            Calculator calc = new Calculator();
            assertEquals(5, calc.add(2, 3));
        }
    }

    JUnit 5 – The Modern Upgrade

    🔹 Key Features:

    • Modular design: JUnit Platform, JUnit Jupiter, and JUnit Vintage.
    • Supports Java 8+ features (lambdas, streams).
    • Improved extension and lifecycle model.
    • More readable annotations like @BeforeEach, @AfterEach, @DisplayName.

    ✅ Example:

    import org.junit.jupiter.api.Test;
    import static org.junit.jupiter.api.Assertions.*;
    
    public class CalculatorTest {
    
        @Test
        void testAddition() {
            Calculator calc = new Calculator();
            assertEquals(5, calc.add(2, 3));
        }
    }

    TestNG – The Enterprise Favorite

    🔹 Key Features:

    • Inspired by JUnit, but adds advanced features.
    • Supports test configuration via XML.
    • Built-in support for parameterized tests, test dependencies, and parallel execution.
    • More flexible for large or enterprise-scale test suites.

    ✅ Example:

    import org.testng.annotations.Test;
    import static org.testng.Assert.assertEquals;

    public class CalculatorTest {

    @Test
    public void testAddition() {
        Calculator calc = new Calculator();
        assertEquals(calc.add(2, 3), 5);
    }
    }

    JUnit 4 vs JUnit 5 vs TestNG: Key Differences

    FeatureJUnit 4JUnit 5TestNG
    Year Introduced200620172004
    Java 8+ Support❌ Limited✅ Full✅ Good
    Parameterized Tests⚠️ Verbose✅ Built-in✅ DataProvider
    Parallel Execution❌ No⚠️ Requires setup✅ Built-in
    Extension Model❌ Basic✅ Powerful✅ Good
    Test Configuration❌ No XML❌ No XML✅ XML support

    Test Lifecycle Annotations: Managing Setup and Teardown

    When writing unit tests, you often need to run some code before or after each test, or even once before/after all tests in a class. This helps in initializing common resources like database connections, test data, or cleaning up afterward.

    Each framework provides lifecycle annotations to handle this setup and teardown process.

    JUnit 4 Annotations

    AnnotationPurpose
    @TestMarks a method as a test method.
    @BeforeRuns before each test method (used for setup).
    @AfterRuns after each test method (used for cleanup).
    @BeforeClassRuns once before all tests in the class (must be static).
    @AfterClassRuns once after all tests in the class (must be static).
    @IgnoreIgnores a test method (skips its execution).
    @RunWithSpecifies a custom runner class to run the tests (e.g., Parameterized.class).
    @FixMethodOrderDefines the order in which test methods are executed.
    @ParametersUsed with parameterized tests to provide test data.
    @RuleAdds additional behavior to tests, such as temporary folder management or timeout rules.

    JUnit 5 Annotations

    AnnotationPurpose
    @TestMarks a method as a test method.
    @BeforeEachRuns before each test method (like JUnit 4’s @Before).
    @AfterEachRuns after each test method (like JUnit 4’s @After).
    @BeforeAllRuns once before all tests in the class (must be static unless using @TestInstance).
    @AfterAllRuns once after all tests in the class (must be static unless using @TestInstance).
    @DisabledDisables (skips) a test method or class (similar to @Ignore in JUnit 4).
    @NestedDefines a nested test class for grouping related tests.
    @TagUsed to tag and filter tests (like categories).
    @DisplayNameDefines a custom name for test classes and methods (for better readability).
    @RepeatedTestRuns the same test multiple times.
    @ParameterizedTestMarks a method as a parameterized test that runs with different arguments.
    @TestInstanceControls test instance lifecycle (PER\_METHOD or PER\_CLASS).
    @TestMethodOrderDefines the order of test method execution.
    @ExtendWithRegisters extensions (similar to runners or rules in JUnit 4).

    TestNg Annotations

    AnnotationPurpose
    @TestMarks a method as a test method.
    @BeforeSuiteRuns once before all tests in the suite.
    @AfterSuiteRuns once after all tests in the suite.
    @BeforeTestRuns before any test methods in the <test> tag in XML configuration.
    @AfterTestRuns after all test methods in the <test> tag.
    @BeforeClassRuns once before the first method in the current class.
    @AfterClassRuns once after all methods in the current class.
    @BeforeMethodRuns before each test method.
    @AfterMethodRuns after each test method.
    @DataProviderMarks a method that provides data for parameterized tests.
    @FactoryMarks a method that returns an array of test class instances for running multiple tests.
    @ListenersDefines listeners for the test class (for logging, reporting, etc.).
    @ParametersInjects parameters from the XML configuration into test methods.

    Common Assertion Statements in Unit Testing

    Below is a comparison of the most commonly used assertions in JUnit 4, JUnit 5, and TestNG, along with code examples.
    🔹 assertEquals(expected, actual)

    Checks if two values are equal.

    JUnit 4:

    import static org.junit.Assert.assertEquals;
    
    assertEquals(5, calculator.add(2, 3));

    JUnit 5:

    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    assertEquals(5, calculator.add(2, 3));

    TestNG:

    import static org.testng.Assert.assertEquals;
    
    assertEquals(calculator.add(2, 3), 5);

    🔹 assertNotEquals(expected, actual)

    Checks if two values are not equal.

    assertNotEquals(6, calculator.add(2, 3))

    Available in JUnit 5 and TestNG.

    Unit 4 requires Hamcrest or additional imports.

    🔹 assertTrue(condition) / assertFalse(condition)

    Checks if a condition is true or false.

    assertTrue(user.isActive());
    assertFalse(user.isBanned());

    All three frameworks support here.

    🔹 assertNull(object) / assertNotNull(object)

    Validates that an object is null or not null.

    assertNull(response.getError());
    assertNotNull(user.getId());

    🔹 assertThrows() (JUnit 5 only)

    Asserts that a specific exception is thrown.

    assertThrows(IllegalArgumentException.class, () -> {
        calculator.divide(10, 0);
    });

    JUnit 5 exclusive.
    In JUnit 4, use @Test(expected = Exception.class)
    In TestNG, use expectedExceptions attribute.

    🔹 Exception Testing in Each Framework
    JUnit 4:

    @Test(expected = IllegalArgumentException.class)
    public void testDivideByZero() {
        calculator.divide(10, 0);
    }

    JUnit 5:

    @Test
    void testDivideByZero() {
        assertThrows(IllegalArgumentException.class, () -> calculator.divide(10, 0));
    }

    TestNG:

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testDivideByZero() {
        calculator.divide(10, 0);
    }

    Summary Table of Assertions

    AssertionJUnit 4JUnit 5TestNG
    assertEquals
    assertNotEquals⚠️ Extra import
    assertTrue
    assertFalse
    assertNull
    assertNotNull
    assertThrows
    Exception Test✅(@Test(expected))✅(assertThrows)✅(expectedExceptions)

    Conclusion

    Unit testing is a key part of building reliable software, making sure the code works correctly before it goes live. JUnit 4 set the foundation with its simple and popular approach. Today, JUnit 5 and TestNG offer modern features that let teams write tests that are more flexible and easier to maintain. Picking the right framework depends on your project’s requirements, any legacy code, and how you plan to grow in the future. Knowing the differences and strengths of each helps developers choose the best tool to improve code quality and speed up development.

    Leave a Comment