Introduction

Testing is the foundation of reliable software, and Java developers have three mature, production-hardened testing frameworks to choose from: JUnit 5 (the modern standard), TestNG (the feature-rich alternative), and Spock (the expressive Groovy-based framework). Each takes a different philosophical approach to writing, organizing, and executing tests.

This guide compares all three frameworks with real test code, covering parameterized tests, parallel execution, data-driven testing, and CI/CD integration for self-hosted Java projects.

Comparison Table

FeatureJUnit 5TestNGSpock
Stars7,0482,0543,631
LanguageJavaJavaGroovy
Test StructureAnnotations (@Test)Annotations (@Test)Specification blocks (given:, when:, then:)
Parameterized Tests@ParameterizedTest + sources@DataProviderwhere: block
Parallel Executionjunit-platform.propertiesXML suite files or annotationsExtensions
Grouping@Tag + @Nested@Test(groups = {...})Features via naming convention
Dependencies@TestMethodOrderdependsOnMethods@Stepwise
AssertionsAssertions.* (fluent)Assert.* (traditional)Expressive conditions (==, thrown())
MockingExternal (Mockito)External (Mockito)Built-in (Mock(), Stub())
ReportingXML via SurefireNative HTML/XMLJunit XML via Maven/Gradle
IDE SupportExcellent (IntelliJ, Eclipse)GoodModerate
Last UpdatedJuly 2026July 2026July 2026

JUnit 5: The Modern Standard

JUnit 5 (7,048 stars) is the gold standard for Java testing. Its architecture splits into three sub-projects — Jupiter (test engine), Vintage (JUnit 3/4 backward compatibility), and Platform (test discovery and execution). This modular design allows extensions, custom test engines, and integration with build tools.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Payment Service Tests")
class PaymentServiceTest {

    private PaymentService service;

    @BeforeEach
    void setUp() {
        service = new PaymentService(new TestPaymentGateway());
    }

    @Test
    @DisplayName("Should process valid payment successfully")
    void shouldProcessValidPayment() {
        PaymentResult result = service.process(
            new PaymentRequest("user123", 99.99, "USD")
        );

        assertAll(
            () -> assertEquals(Status.SUCCESS, result.getStatus()),
            () -> assertNotNull(result.getTransactionId()),
            () -> assertTrue(result.getProcessingTimeMs() < 5000,
                "Payment processing should complete within 5 seconds")
        );
    }

    @ParameterizedTest
    @CsvSource({
        "0.01, SUCCESS",
        "99999.99, SUCCESS",
        "0.00, INVALID_AMOUNT",
        "-50.00, INVALID_AMOUNT"
    })
    @DisplayName("Should handle various payment amounts")
    void shouldHandlePaymentAmounts(double amount, Status expectedStatus) {
        PaymentResult result = service.process(
            new PaymentRequest("user123", amount, "USD")
        );
        assertEquals(expectedStatus, result.getStatus());
    }

    @Nested
    @DisplayName("Refund Scenarios")
    class RefundTests {
        @Test
        void shouldRefundCompletedPayment() {
            PaymentResult payment = service.process(
                new PaymentRequest("user123", 50.00, "USD")
            );
            RefundResult refund = service.refund(payment.getTransactionId());
            assertEquals(RefundStatus.COMPLETED, refund.getStatus());
        }
    }
}

Strengths: Best IDE support (IntelliJ, Eclipse, VS Code), extensive extension ecosystem (Mockito, AssertJ), @Nested for organized test hierarchies, assertAll() for comprehensive assertions. Weaknesses: No built-in test dependencies (unlike TestNG), parameterized test sources can feel verbose compared to Spock’s where: blocks.

TestNG: Feature-Rich Powerhouse

TestNG (2,054 stars) was created to address perceived JUnit 4 limitations — particularly around test dependencies, grouping, and parallel execution. It remains popular in large enterprise projects and Selenium test suites.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import org.testng.annotations.*;
import org.testng.Assert;

@Test(groups = {"payment", "integration"})
public class PaymentServiceTest {

    private PaymentService service;

    @BeforeClass
    public void initService() {
        service = new PaymentService(new TestPaymentGateway());
    }

    @Test(
        description = "Process payment with valid card",
        priority = 1,
        invocationCount = 3,
        threadPoolSize = 3
    )
    public void testProcessValidPayment() {
        PaymentResult result = service.process(
            new PaymentRequest("user123", 99.99, "USD")
        );
        Assert.assertEquals(result.getStatus(), Status.SUCCESS);
        Assert.assertNotNull(result.getTransactionId());
    }

    @DataProvider(name = "paymentAmounts")
    public Object[][] paymentAmounts() {
        return new Object[][] {
            {0.01, Status.SUCCESS},
            {99999.99, Status.SUCCESS},
            {0.00, Status.INVALID_AMOUNT},
            {-50.00, Status.INVALID_AMOUNT}
        };
    }

    @Test(dataProvider = "paymentAmounts", dependsOnMethods = "testProcessValidPayment")
    public void testPaymentAmounts(double amount, Status expectedStatus) {
        PaymentResult result = service.process(
            new PaymentRequest("user123", amount, "USD")
        );
        Assert.assertEquals(result.getStatus(), expectedStatus);
    }

    @AfterClass
    public void cleanup() {
        service.shutdown();
    }
}

Strengths: Built-in parallel execution (threadPoolSize), test dependencies (dependsOnMethods), flexible grouping (groups), data-driven testing via @DataProvider. Weaknesses: Smaller community than JUnit 5, IDE support less polished, annotations can become verbose for complex workflows.

Spock: Behavior-Driven Testing

Spock (3,631 stars) is a Groovy-based testing framework that reads like English prose. Its specification-style test blocks (given:, when:, then:, where:) make tests self-documenting and accessible to non-developers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import spock.lang.*

class PaymentServiceSpec extends Specification {

    PaymentService service
    TestPaymentGateway gateway

    def setup() {
        gateway = Mock(TestPaymentGateway)
        service = new PaymentService(gateway)
    }

    def "should process valid payment and return transaction ID"() {
        given: "a valid payment request"
        def request = new PaymentRequest(
            userId: "user123",
            amount: 99.99,
            currency: "USD"
        )

        when: "the payment is processed"
        def result = service.process(request)

        then: "the gateway is called exactly once"
        1 * gateway.charge(request) >> new GatewayResponse(
            approved: true,
            transactionId: "txn-456"
        )

        and: "the result indicates success"
        result.status == Status.SUCCESS
        result.transactionId == "txn-456"
    }

    @Unroll
    def "should handle payment of #amount #currency → #expectedStatus"() {
        given: "a payment request"
        def request = new PaymentRequest(
            userId: "user123",
            amount: amount,
            currency: currency
        )

        when: "processing the payment"
        def result = service.process(request)

        then: "the status matches expectation"
        result.status == expectedStatus

        where:
        amount   | currency | expectedStatus
        0.01     | "USD"    | Status.SUCCESS
        99999.99 | "USD"    | Status.SUCCESS
        0.00     | "USD"    | Status.INVALID_AMOUNT
        -50.00   | "EUR"    | Status.INVALID_AMOUNT
    }

    def "should throw PaymentException for gateway timeout"() {
        given: "a slow gateway"
        gateway.charge(_) >> { throw new TimeoutException() }

        when: "processing a payment"
        service.process(new PaymentRequest(amount: 50.00))

        then: "a PaymentException is thrown"
        thrown(PaymentException)
    }
}

Strengths: Most readable test syntax, built-in mocking (Mock(), Stub()), data-driven testing with expressive where: blocks, @Unroll for readable parameterized test names. Weaknesses: Requires Groovy dependency, slower compilation than Java, IDE support requires Groovy plugin, steeper learning curve for Java-only teams.

Choosing the Right Framework

  • New Java Projects: JUnit 5 — it’s the modern standard, has the best tooling support, and is actively maintained. Use Mockito and AssertJ alongside it.
  • Enterprise Selenium Suites: TestNG — its built-in parallel execution, test dependencies, and grouping features are designed for complex integration test suites with hundreds of test classes.
  • BDD and Specification Teams: Spock — when test readability matters as much as correctness, Spock’s given-when-then blocks communicate intent to product owners and QA engineers.
  • Mixed Kotlin/Java Codebases: JUnit 5 — it supports Kotlin natively and integrates with Kotest assertions for Kotlin-specific test styles.

CI/CD Integration

All three frameworks integrate seamlessly with Maven and Gradle for self-hosted CI pipelines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!-- Maven Surefire configuration for parallel JUnit 5 -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
        <parallel>classes</parallel>
        <threadCount>4</threadCount>
        <properties>
            <configurationParameters>
                junit.jupiter.execution.parallel.enabled = true
                junit.jupiter.execution.parallel.mode.default = concurrent
            </configurationParameters>
        </properties>
    </configuration>
</plugin>

For building complete Java applications, see our Java Web Frameworks comparison and our Java CLI Libraries guide for command-line tool testing patterns. For HTTP client testing, our Java HTTP Client Libraries guide covers OkHttp and Retrofit mocking strategies.

Test Automation Strategy and CI Pipeline Design

A testing framework is only as effective as its integration into the development workflow. Self-hosted CI/CD pipelines must balance thoroughness against build time — a 30-minute test suite discourages frequent commits, while a 30-second suite misses critical regressions.

Test categorization is the foundation. All three frameworks support test grouping through different mechanisms: JUnit 5 uses @Tag("unit") and @Tag("integration") annotations, TestNG uses @Test(groups = {"unit"}), and Spock uses naming conventions with @Category or package-level filtering. Categorize tests into three tiers: unit (milliseconds, no I/O), integration (seconds, real database), and end-to-end (minutes, full stack).

Parallel execution dramatically reduces build times. JUnit 5 requires junit.jupiter.execution.parallel.enabled=true in junit-platform.properties plus Maven Surefire’s <parallel>classes</parallel> configuration. TestNG handles parallelism natively with threadPoolSize on @Test annotations and <suite> XML configurations — a key advantage for teams with hundreds of integration test classes. Spock tests can run in parallel through Gradle’s maxParallelForks or Maven Surefire’s fork configuration, though Spock’s @Shared fields require careful handling in parallel mode.

Fork-based isolation prevents test pollution. Maven Surefire’s forkCount parameter spawns a new JVM for each test class (or group of classes), ensuring no static state leaks between test suites. For database integration tests, use Testcontainers to spin up isolated PostgreSQL, MySQL, or Redis instances in Docker — each test class gets its own database, eliminating cross-test data dependencies. Testcontainers integrates seamlessly with all three frameworks.

Flaky test management becomes critical at scale. All three frameworks support retry mechanisms: JUnit 5’s @RepeatedTest and TestReporter, TestNG’s retryAnalyzer, and Spock’s @Retry extension. A smarter approach is collecting flaky test metrics over multiple CI runs and automatically quarantining tests that fail intermittently. Tools like DeFlaker (academic) and commercial CI analytics platforms help identify non-deterministic tests that waste developer time.

A well-designed CI pipeline for a self-hosted Java project typically runs three stages: fast unit tests on every commit (under 2 minutes), integration tests on PR creation (under 10 minutes), and full end-to-end suites nightly. JUnit 5 is the default choice for all three stages, but teams with complex data-driven integration suites often run TestNG in parallel alongside JUnit 5 unit tests, leveraging each framework’s strengths where they matter most.

FAQ

Which testing framework is best for Spring Boot applications?

JUnit 5 is the de facto standard for Spring Boot — spring-boot-starter-test includes JUnit 5 by default with Spring Test, Mockito, and AssertJ. Spring Boot’s @SpringBootTest, @WebMvcTest, and @DataJpaTest all work with JUnit 5 annotations seamlessly.

Can I use Spock without knowing Groovy?

You’ll need basic Groovy syntax familiarity — method definitions, closures, data types. The testing DSL (given:, when:, then:) is self-documenting and easy to learn even without deep Groovy knowledge. Start with simple specs and add Groovy features incrementally.

How do I migrate from JUnit 4 to JUnit 5?

Use the JUnit Vintage engine for backward compatibility — it runs JUnit 4 tests on the JUnit 5 platform. Migrate tests incrementally: change @Test imports from org.junit to org.junit.jupiter.api, replace @Before with @BeforeEach, and update assertion syntax. IDE refactoring tools can automate most of this.

Does TestNG still have advantages over JUnit 5?

Yes — TestNG’s @DataProvider with parallel execution, test-level dependencies (dependsOnMethods), and built-in parallel test execution without additional configuration remain its key differentiators. For complex integration test suites with hundreds of interdependent tests, TestNG’s features are unmatched.

What mocking library should I use with JUnit 5?

Mockito is the standard choice — version 5+ integrates with JUnit 5 via @ExtendWith(MockitoExtension.class). For Kotlin projects, MockK provides Kotlin-native mocking. Spock has built-in mocking and doesn’t need an external library.

Are these frameworks suitable for Android testing?

JUnit 5 works for JVM unit tests in Android projects but Android instrumentation tests still use JUnit 4 (AndroidX Test). TestNG and Spock require additional setup and are less common in Android projects due to method count and build system constraints.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com