Project Brief — Unit + Integration Test Suite for a Calculator Service

10 min read

You have covered every major capability JUnit 5 offers: the lifecycle model, the complete assertion toolkit, parameterised tests with four source types, the extension model with custom ParameterResolver and TestWatcher, conditional execution, parallel configuration, Selenium integration, Surefire configuration, and reporting. The capstone ties all of that together in one cohesive project rather than a collection of isolated exercises.

The project is a test suite for a CalculatorService — both the business logic (unit tests) and a REST API wrapper (integration tests). The service is simple enough to implement from scratch in an afternoon, but rich enough to exercise every technique this course has taught. By the end you will have a project you can show in a portfolio or use as a template for real work.

The system under test

CalculatorService — a Java class you implement:

public class CalculatorService {
 
    public double add(double a, double b)      { return a + b; }
    public double subtract(double a, double b) { return a - b; }
    public double multiply(double a, double b) { return a * b; }
 
    public double divide(double a, double b) {
        if (b == 0) throw new ArithmeticException("Division by zero");
        return a / b;
    }
 
    public double power(double base, int exponent) {
        if (exponent < 0) throw new IllegalArgumentException("Negative exponent not supported");
        return Math.pow(base, exponent);
    }
 
    public double sqrt(double value) {
        if (value < 0) throw new IllegalArgumentException("Square root of negative number");
        return Math.sqrt(value);
    }
}

REST API — a simple HTTP endpoint. You can implement this with Spring Boot (if you have it available), use a mock server library like WireMock, or stub the HTTP layer with RestAssured's mock DSL. The API surface is:

  • POST /calculate — body: {"operation": "add", "a": 10, "b": 5} — response: {"result": 15.0}
  • POST /calculate with "operation": "divide" and b: 0 — response: {"error": "Division by zero"} with status 400

If you do not have Spring Boot available, implement the service layer only — the unit tests are the core deliverable. The integration tests are a stretch goal.

Deliverables

Build the following, in this order. Each item maps to a chapter in this course.

1. Maven project foundation (Chapter 1)

A clean pom.xml with junit-jupiter 5.10.2, Surefire 3.2.5, allure-junit5 2.25.0, and optionally selenium-java and rest-assured. Two source roots: src/main/java for CalculatorService, src/test/java for the test suite.

2. Parameterised unit tests (Chapter 3)

Use @ParameterizedTest with @CsvSource to cover all operations. Each operation gets its own set of input/expected pairs:

@ParameterizedTest(name = "{0} + {1} = {2}")
@CsvSource({
    "0,   0,   0",
    "1,   1,   2",
    "-5,  5,   0",
    "1.5, 2.5, 4.0",
    "100, -50, 50"
})
void addition(double a, double b, double expected) {
    assertEquals(expected, calculator.add(a, b), 1e-9);
}

Repeat for subtract, multiply, divide, power, and sqrt. Use @MethodSource for the divide-by-zero and negative-sqrt cases where you need to pass object types or multiple exception scenarios.

3. @Nested test classes (Chapter 2)

Organise the unit tests into nested classes — one per operation — so the report reads as a specification:

CalculatorServiceTest
  Addition
    ✅ 0 + 0 = 0
    ✅ 1 + 1 = 2
    ✅ -5 + 5 = 0
  Division
    ✅ 10 / 2 = 5.0
    ❌ divide by zero throws ArithmeticException

4. assertAll for API response validation (Chapter 2)

When testing the REST endpoint, validate status code, content type, and body fields simultaneously:

assertAll("POST /calculate — add",
    () -> assertEquals(200, response.getStatusCode()),
    () -> assertEquals("application/json", response.getContentType()),
    () -> assertEquals(15.0, response.jsonPath().getDouble("result"), 1e-9)
);

5. @RepeatedTest for consistency (Chapter 3)

The sqrt operation uses Math.sqrt internally — it should always return the same value. Prove consistency:

@RepeatedTest(10)
void sqrtIsConsistent() {
    assertEquals(4.0, calculator.sqrt(16.0), 1e-9);
}

Also add a @RepeatedTest(20) for the divide method to confirm there is no floating-point drift across repeated calls with the same inputs.

6. Custom extension — timing and logging (Chapter 4)

Write a CalculatorTestExtension that implements BeforeTestExecutionCallback, AfterTestExecutionCallback, and TestWatcher. It should:

  • Record the start time in BeforeTestExecutionCallback
  • Print [TIMING] TestName: Xms in AfterTestExecutionCallback
  • Print ✅ PASSED, ❌ FAILED (message), or ⏭ SKIPPED from TestWatcher

Register it globally via META-INF/services so every test in the suite gets timing output automatically.

7. Integration tests with @Tag (Chapters 2 and 5)

Tag all unit tests with @Tag("unit") and all API integration tests with @Tag("integration"). Configure Surefire to run only @Tag("unit") tests during mvn test:

<configuration>
    <groups>unit</groups>
</configuration>

Run integration tests separately: mvn test -Dgroups=integration. Or use Failsafe with *IT.java naming for the integration tests.

8. Surefire and Failsafe configuration (Chapter 5)

Wire up a Maven profile for each environment:

<profiles>
    <profile>
        <id>ci</id>
        <properties>
            <baseUrl>https://api.staging.myapp.com</baseUrl>
        </properties>
        <build>
            <plugins>
                <!-- Surefire with parallel for unit tests -->
                <!-- Failsafe for integration tests -->
            </plugins>
        </build>
    </profile>
</profiles>

9. Allure reporting (Chapter 5)

Annotate test classes with @Feature("Calculator Operations"). Annotate individual tests with @Story and @Severity. Add @Step to any helper methods in the integration tests. Verify that allure serve target/allure-results shows a Behaviors view with your feature groupings.

10. CI pipeline (Chapter 5)

A GitHub Actions workflow (or equivalent) that:

  • Runs mvn test -Pci on push to any branch
  • Uploads target/surefire-reports/ as an artifact with if: always()
  • Uploads target/allure-results/ for report generation
  • Fails the job when any unit test fails

Suggested file structure

calculator-tests/
├── pom.xml
├── src/
│   ├── main/java/com/mycompany/calculator/
│   │   └── CalculatorService.java
│   └── test/
│       ├── java/com/mycompany/calculator/
│       │   ├── CalculatorServiceTest.java        ← @Nested unit tests
│       │   ├── CalculatorApiIT.java              ← integration tests
│       │   ├── extensions/
│       │   │   └── CalculatorTestExtension.java
│       │   └── testdata/
│       │       └── CalculatorTestData.java       ← @MethodSource factories
│       └── resources/
│           ├── junit-platform.properties         ← parallel config
│           ├── testdata/
│           │   └── operations.csv               ← @CsvFileSource data
│           └── META-INF/services/
│               └── org.junit.jupiter.api.extension.Extension
└── .github/workflows/
    └── test.yml

Deliverable map

Capstone Project
  • – @ParameterizedTest with @CsvSource
  • – @Nested per operation
  • – @RepeatedTest for consistency
  • – assertEquals with delta for doubles
  • – assertThrows for divide-by-zero
  • – assertAll for API responses
  • – CalculatorTestExtension (timing + log)
  • – Global registration via META-INF
  • – ScreenshotExtension for Selenium (stretch)
  • Surefire + Failsafe split –
  • Maven profiles: local / ci –
  • Allure with @Feature, @Story, @Step –
  • GitHub Actions workflow –
  • Artifacts: surefire XML + allure results –
  • @Tag filtering: unit vs integration –

Time estimate

DeliverableEstimated time
CalculatorService implementation20 min
Parameterised unit tests (all operations)45 min
@Nested structure + assertAll20 min
@RepeatedTest + custom extension25 min
Maven / Surefire / Failsafe config20 min
Allure annotations + report20 min
GitHub Actions CI20 min
Integration tests (stretch)45 min

Core deliverables (no integration tests): approximately 3 hours. Full project with integration tests and CI: approximately 4–5 hours. Work through them one deliverable at a time — each one is independently verifiable with mvn test.

Next lesson: guided walkthrough of the implementation, with complete code for the key components.

// tip to track lessons you complete and pick up where you left off across devices.