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 /calculatewith"operation": "divide"andb: 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: XmsinAfterTestExecutionCallback - Print
✅ PASSED,❌ FAILED (message), or⏭ SKIPPEDfromTestWatcher
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 -Pcion push to any branch - Uploads
target/surefire-reports/as an artifact withif: 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
- – @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
| Deliverable | Estimated time |
|---|---|
| CalculatorService implementation | 20 min |
| Parameterised unit tests (all operations) | 45 min |
| @Nested structure + assertAll | 20 min |
| @RepeatedTest + custom extension | 25 min |
| Maven / Surefire / Failsafe config | 20 min |
| Allure annotations + report | 20 min |
| GitHub Actions CI | 20 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.