Software Testing & QA Tools
JUnit and TestNG
JUnit is the default unit-testing framework for Java: annotation-driven (@Test, @BeforeEach, @AfterEach), tightly integrated with Maven, Gradle, Spring Boot, and every major IDE. Its JUnit 5 generation (the 'Jupiter' programming model) is what most codebases target today, and JUnit 6 — released in September 2025 — carries that same model forward while unifying versioning across its Platform, Jupiter, and Vintage modules and raising the minimum Java version to 17. TestNG was created specifically to cover gaps in older JUnit versions: it adds native test dependencies, flexible grouping, data-driven tests via @DataProvider, and built-in parallel execution, which made it the historical choice for functional and Selenium-driven UI suites even as JUnit closed much of that gap over time.
Why it matters
- JUnit is the assumed default across the Java ecosystem
- Spring Boot's default test starter, Maven Surefire's out-of-the-box configuration, and most Java tutorials all assume JUnit (specifically the Jupiter engine) unless a project deliberately chooses otherwise.
- TestNG's data-driven and parallel-execution features made it common in Selenium and functional suites
- @DataProvider feeds a single test method many input rows natively, and a testng.xml suite file can configure parallel execution at the method, class, or suite level without any extra plugin.
- The two aren't mixed inside one test class
- They use different annotations and lifecycle names — @BeforeEach in JUnit versus @BeforeMethod in TestNG — so a codebase generally standardizes on one, and running both means maintaining two separate reporting and configuration setups in the same build.
- Framework choice shapes what the CI pipeline actually reports
- Maven Surefire and Failsafe produce slightly different report output depending on which engine ran, and IDE test-runner integration, along with reporting tools like Allure, plugs into each framework somewhat differently.
Where JUnit 5 (and now JUnit 6) stands today
The Jupiter programming model most Java developers already know is @Test for a single case, @BeforeEach/@AfterEach for per-test setup and teardown, @BeforeAll/@AfterAll for once-per-class setup, @ParameterizedTest paired with a data source annotation (@CsvSource, @MethodSource, and others) for data-driven tests, @Nested for grouping related tests inside a class, and @ExtendWith for plugging in custom lifecycle behavior. JUnit 6, released in September 2025, keeps this same model — existing JUnit 5 test code generally keeps working — while unifying JUnit Platform, Jupiter, and Vintage under one shared version number and raising the minimum required Java version to 17. The Vintage engine lets old JUnit 3 and 4 tests run on the same modern platform during a migration, but as of JUnit 6 it's explicitly marked deprecated (it now logs a discovery warning if it finds any JUnit 4 test) and is meant only as a temporary bridge, not a long-term target for new tests.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorTest {
@Test
void addsTwoNumbers() {
assertEquals(5, Calculator.add(2, 3));
}
@ParameterizedTest
@CsvSource({"2,3,5", "10,-1,9", "0,0,0"})
void addsVariousPairs(int a, int b, int expected) {
assertEquals(expected, Calculator.add(a, b));
}
}TestNG's answer to the same problems
TestNG uses its own annotation set — @BeforeMethod/@AfterMethod and @BeforeClass/@AfterClass like JUnit, but also @BeforeSuite/@AfterSuite and @BeforeTest/@AfterTest, giving a four-level hierarchy (suite, test, class, method) that's more granular than JUnit's essentially class-and-method model. @DataProvider supplies data-driven tests without a second annotation the way JUnit needs @ParameterizedTest plus a source. dependsOnMethods and dependsOnGroups let a test declare that it must run after specific other tests or groups have passed — a deliberate feature, whereas JUnit's design philosophy actively discourages inter-test dependencies since tests are meant to be order-independent. A testng.xml file configures which groups run, exclusions, and parallelism from outside the code, without touching test classes.
import org.testng.annotations.Test;
import org.testng.annotations.DataProvider;
import static org.testng.Assert.assertEquals;
public class CalculatorTest {
@DataProvider(name = "pairs")
public Object[][] pairs() {
return new Object[][] { {2, 3, 5}, {10, -1, 9}, {0, 0, 0} };
}
@Test(dataProvider = "pairs")
public void addsVariousPairs(int a, int b, int expected) {
assertEquals(Calculator.add(a, b), expected);
}
}Parallel execution and test dependencies
TestNG has supported parallel execution natively for a long time, configured centrally through testng.xml's parallel attribute (methods, classes, tests, or instances) and a thread-count, with no extra plugin required. JUnit 5 added configurable parallel execution later (via junit.jupiter.execution.parallel.enabled in a junit-platform.properties file), off by default, and getting fine-grained control still means annotating classes with @Execution(ExecutionMode.CONCURRENT) or similar. TestNG's dependsOnMethods is the more direct tool when a suite genuinely needs ordered execution, at the cost of making those specific tests harder to run in true isolation — which is exactly the coupling JUnit's design tries to avoid by not offering the feature at all.
What actually differs day to day
Annotation names are the most visible difference and the most common source of copy-paste mistakes when switching between the two (or between JUnit 4 and JUnit 5, which also renamed several). Both ship a basic Assertions/Assert class, and most serious projects using either framework add a fluent assertion library like AssertJ or Hamcrest on top for more readable multi-field checks than either framework's built-in assertions provide on their own.
Mistakes people make here
- Mixing JUnit 4 and JUnit 5 (Jupiter) annotations in the same class
- @Before/@After (from org.junit, JUnit 4) and @BeforeEach/@AfterEach (from org.junit.jupiter.api, JUnit 5) look similar but come from different packages — using the JUnit 4 one in a class run by the Jupiter engine doesn't cause a compile error, it's just silently ignored, unless the deprecated Vintage engine is also on the classpath to interpret it.
- Writing TestNG tests that rely on an implicit execution order
- TestNG doesn't guarantee declaration-order execution any more than JUnit does — a test that depends on state a previous test happened to leave behind needs an explicit dependsOnMethods or dependsOnGroups declaration, not an assumption that TestNG will just run things in file order.
- Treating a failing @DataProvider or @ParameterizedTest row as 'the test is flaky'
- Both report each input set as its own separately named test invocation, so a single failing row is a specific, reproducible input — not intermittent flakiness — but CI summaries that collapse parametrized results into one line can make it look that way if nobody expands the detail.
- Choosing TestNG for a new Selenium or integration suite purely out of habit, without checking what the rest of the project uses
- Running both frameworks in one repository means maintaining two sets of Maven Surefire/Failsafe configuration, two IDE test-runner setups, and two reporting outputs for what is otherwise one test suite — a real, ongoing maintenance cost for a decision usually made once, early, without much thought.
Strengths and trade-offs
Where it is strong
- JUnit 5's default integration with Spring Boot, Maven, Gradle, and IDEs means it needs the least setup for most new Java projects.
- TestNG's @DataProvider and dependsOnMethods/dependsOnGroups give data-driven and dependency-based testing without any extra library — JUnit needs @ParameterizedTest plus a source annotation for the first, and doesn't offer declared dependencies by design.
- Both have mature parallel-execution support today, though TestNG's has existed natively for longer and is configured centrally in testng.xml rather than through JVM system properties.
- JUnit 5's @Nested and @ExtendWith give a composable way to group related tests and inject custom setup logic, which many teams find maps more naturally onto how they already organize test classes.
The trade-offs
- Moving an existing suite between JUnit 4 and JUnit 5, or between TestNG and JUnit, isn't a drop-in annotation swap — package names, some lifecycle semantics, and assertion classes all differ, so migration is genuine work, not a find-and-replace.
- TestNG's explicit test dependencies make ordered suites easier to build, but they also make individual tests harder to run in true isolation, cutting against the general principle that tests shouldn't depend on each other.
- JUnit is the ecosystem default, so TestNG projects draw on a smaller pool of tutorials, Stack Overflow answers, and IDE-specific edge-case tooling.
- Neither framework ships a fluent assertion library as expressive as AssertJ out of the box — most serious projects using either one add AssertJ or Hamcrest anyway.
Who needs this
Any Java developer writing tests needs JUnit at minimum, since it's the ecosystem default; TestNG is worth knowing for teams maintaining older Selenium or functional suites, or that specifically need native test dependencies and data providers without adding extra libraries.
Questions about junit and testng
- Should a new Java project pick JUnit or TestNG?
- JUnit 5 (Jupiter) is the safer default today because of how deeply it's integrated with Spring Boot, build tools, and IDEs. TestNG remains a reasonable choice if a team specifically wants native test dependencies or data providers without extra libraries, or is already standardized on it for an existing Selenium suite.
- Is JUnit 4 still relevant?
- Existing JUnit 4 suites keep running via the Vintage engine on top of the modern JUnit Platform, but Vintage is explicitly deprecated and meant only as a temporary migration aid — new tests should be written against Jupiter (JUnit 5/6), not JUnit 4.
- Can JUnit and TestNG run in the same Maven or Gradle build?
- Technically yes, since both integrate with Surefire and Failsafe, but it means maintaining two separate reporting and configuration setups for what functions as one test suite — most projects standardize on a single framework rather than run both long-term.
- What replaced JUnit 5 as the current version?
- JUnit 6, released in September 2025, keeps the same Jupiter programming model most JUnit 5 code already uses, while unifying Platform, Jupiter, and Vintage under one shared version number and raising the minimum supported Java version to 17.