chore: Update build configuration for Scoverage and ScalaTest integration
This commit is contained in:
@@ -9,7 +9,7 @@ You do not have permissions to modify the source code, just write tests.
|
|||||||
You write tests for Scala 3 + Quarkus services.
|
You write tests for Scala 3 + Quarkus services.
|
||||||
|
|
||||||
## Test style
|
## Test style
|
||||||
- Unit tests: `extends AnyFunSuite with Matchers with JUnitSuiteLike` — use `test("description") { ... }` DSL, no `@Test` annotation, no `: Unit` return type needed.
|
- Unit tests: `extends AnyFunSuite with Matchers` — use `test("description") { ... }` DSL, no `@Test` annotation, no `: Unit` return type needed.
|
||||||
- Integration tests: `@QuarkusTest` with JUnit 5 — `@Test` methods MUST be explicitly typed `: Unit`.
|
- Integration tests: `@QuarkusTest` with JUnit 5 — `@Test` methods MUST be explicitly typed `: Unit`.
|
||||||
|
|
||||||
Target 95%+ conditional coverage.
|
Target 95%+ conditional coverage.
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
---
|
|
||||||
name: contract-first-test-writing
|
|
||||||
description: Use when the architect has produced an OpenAPI contract but scala-implementer has not yet written any source code - write failing tests from the contract so implementation has a target to satisfy
|
|
||||||
---
|
|
||||||
|
|
||||||
# Contract-First Test Writing (TDD Red Phase)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Write tests from the API contract **before** any implementation exists. Tests will fail — that is correct and expected. The scala-implementer's job is to make them green.
|
|
||||||
|
|
||||||
**Iron Law:** Never look at `src/main/scala`. If it exists, ignore it. Derive every assertion from `docs/api/{service}.yaml` and the relevant ADR in `docs/adr/`.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
### 1. Read the contract
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/api/{service-name}.yaml ← OpenAPI spec (required)
|
|
||||||
docs/adr/ ← ADRs for domain rules and data shapes
|
|
||||||
```
|
|
||||||
|
|
||||||
Extract for each endpoint:
|
|
||||||
- HTTP method + path
|
|
||||||
- Request body shape and required fields
|
|
||||||
- Response status codes and body shape
|
|
||||||
- Error cases (4xx, 5xx) documented in the spec
|
|
||||||
|
|
||||||
### 2. Write `@QuarkusTest` integration tests (one per endpoint)
|
|
||||||
|
|
||||||
Cover for every endpoint:
|
|
||||||
|
|
||||||
| Scenario | What to assert |
|
|
||||||
|----------|---------------|
|
|
||||||
| Happy path | Correct 2xx status + response body shape |
|
|
||||||
| Missing required field | 400 response |
|
|
||||||
| Invalid input | 400 or 422 response |
|
|
||||||
| Not found | 404 response (where applicable) |
|
|
||||||
| Error contract | Response body matches error schema |
|
|
||||||
|
|
||||||
```scala
|
|
||||||
import io.quarkus.test.junit.QuarkusTest
|
|
||||||
import io.restassured.RestAssured.given
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import jakarta.ws.rs.core.MediaType
|
|
||||||
|
|
||||||
@QuarkusTest
|
|
||||||
class MoveEndpointTest:
|
|
||||||
|
|
||||||
@Test
|
|
||||||
def validMove_returns200(): Unit =
|
|
||||||
given()
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.body("""{"from":"e2","to":"e4"}""")
|
|
||||||
.when()
|
|
||||||
.post("/api/moves")
|
|
||||||
.`then`()
|
|
||||||
.statusCode(200)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
def missingField_returns400(): Unit =
|
|
||||||
given()
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.body("""{"from":"e2"}""")
|
|
||||||
.when()
|
|
||||||
.post("/api/moves")
|
|
||||||
.`then`()
|
|
||||||
.statusCode(400)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Write unit tests for domain rules
|
|
||||||
|
|
||||||
For every domain invariant described in the ADR (validation rules, state machines, error conditions), write a ScalaTest unit test:
|
|
||||||
|
|
||||||
```scala
|
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
|
||||||
import org.scalatest.matchers.should.Matchers
|
|
||||||
import org.scalatestplus.junit.JUnitSuiteLike
|
|
||||||
|
|
||||||
class MoveValidatorTest extends AnyFunSuite with Matchers with JUnitSuiteLike:
|
|
||||||
|
|
||||||
test("invalid square is rejected") {
|
|
||||||
val result = MoveValidator.validate("z9", "e4")
|
|
||||||
assert(result.isLeft)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Confirm tests compile but fail
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew :modules:{service-name}:test
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected outcome: **compilation succeeds, tests fail** (no implementation yet).
|
|
||||||
|
|
||||||
If compilation fails, fix the test code — do not create implementation code.
|
|
||||||
|
|
||||||
If tests somehow pass, the contract is already implemented; notify the team-lead.
|
|
||||||
|
|
||||||
### 5. Hand off to scala-implementer
|
|
||||||
|
|
||||||
Leave a comment at the top of the primary test file:
|
|
||||||
|
|
||||||
```scala
|
|
||||||
// RED: These tests define the contract for {service-name}.
|
|
||||||
// scala-implementer: make them green without modifying test assertions.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- **No peeking at `src/main/scala`** — tests must be derived from the contract only.
|
|
||||||
- Use `@QuarkusTest` + REST Assured for HTTP endpoints — `@Test` methods must be explicitly typed `: Unit`.
|
|
||||||
- Use `AnyFunSuite with Matchers with JUnitSuiteLike` for pure domain logic unit tests — no `@Test`, no `: Unit` needed.
|
|
||||||
- Do not mock the implementation — tests call real endpoints, real domain code.
|
|
||||||
- Do not write happy-path-only tests; every documented error case needs a test.
|
|
||||||
|
|
||||||
## After Implementation: Coverage Check
|
|
||||||
|
|
||||||
Once scala-implementer is done and tests are green, run the coverage reporter to find any gaps the contract tests missed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 jacoco-reporter/jacoco_coverage_gaps.py \
|
|
||||||
modules/{service-name}/build/reports/jacoco/test/jacocoTestReport.xml \
|
|
||||||
--output agent
|
|
||||||
```
|
|
||||||
|
|
||||||
Use the `jacoco-coverage-gaps` skill to close remaining gaps.
|
|
||||||
+3
-1
@@ -40,4 +40,6 @@ bin/
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/jacoco-reporter/.venv/
|
||||||
|
/.claude/settings.local.json
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ScalaCompilerConfiguration">
|
<component name="ScalaCompilerConfiguration">
|
||||||
<profile name="Gradle 1" modules="NowChessSystems.modules.api.main,NowChessSystems.modules.api.test">
|
<profile name="Gradle 1" modules="NowChessSystems.modules.api.main,NowChessSystems.modules.api.scoverage,NowChessSystems.modules.api.test">
|
||||||
<option name="deprecationWarnings" value="true" />
|
<option name="deprecationWarnings" value="true" />
|
||||||
<option name="uncheckedWarnings" value="true" />
|
<option name="uncheckedWarnings" value="true" />
|
||||||
</profile>
|
</profile>
|
||||||
<profile name="Gradle 2" modules="NowChessSystems.modules.core.main,NowChessSystems.modules.core.test">
|
<profile name="Gradle 2" modules="NowChessSystems.modules.core.main,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test">
|
||||||
<option name="deprecationWarnings" value="true" />
|
<option name="deprecationWarnings" value="true" />
|
||||||
<option name="uncheckedWarnings" value="true" />
|
<option name="uncheckedWarnings" value="true" />
|
||||||
<parameters>
|
<parameters>
|
||||||
|
|||||||
+2
-1
@@ -6,7 +6,8 @@ val versions = mapOf(
|
|||||||
"SCALA3" to "3.5.1",
|
"SCALA3" to "3.5.1",
|
||||||
"SCALA_LIBRARY" to "2.13.18",
|
"SCALA_LIBRARY" to "2.13.18",
|
||||||
"SCALATEST" to "3.2.19",
|
"SCALATEST" to "3.2.19",
|
||||||
"SCALATESTPLUS_JUNIT5" to "3.2.19.1"
|
"SCALATESTPLUS_JUNIT5" to "3.2.19.0",
|
||||||
|
"SCOVERAGE" to "2.1.1"
|
||||||
)
|
)
|
||||||
extra["VERSIONS"] = versions
|
extra["VERSIONS"] = versions
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
jacoco
|
|
||||||
id("scala")
|
id("scala")
|
||||||
|
id("org.scoverage") version "8.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "de.nowchess"
|
group = "de.nowchess"
|
||||||
@@ -14,14 +14,19 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scala {
|
scala {
|
||||||
versions["SCALA3"]!!
|
scalaVersion = versions["SCALA3"]!!
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
scoverage {
|
||||||
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
|
scoverageVersion.set(versions["SCOVERAGE"]!!)
|
||||||
}
|
}
|
||||||
tasks.jacocoTestReport {
|
|
||||||
dependsOn(tasks.test) // tests are required to run before generating the report
|
configurations.scoverage {
|
||||||
|
resolutionStrategy.eachDependency {
|
||||||
|
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
|
||||||
|
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -42,11 +47,22 @@ dependencies {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testImplementation(platform("org.junit:junit-bom:5.10.0"))
|
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
||||||
|
testImplementation("org.scalatestplus:junit-5-13_3:${versions["SCALATESTPLUS_JUNIT5"]!!}")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform {
|
||||||
|
includeEngines("scalatest")
|
||||||
|
testLogging {
|
||||||
|
events("passed", "skipped", "failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalizedBy(tasks.reportScoverage)
|
||||||
|
}
|
||||||
|
tasks.reportScoverage {
|
||||||
|
dependsOn(tasks.test)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("scala")
|
id("scala")
|
||||||
jacoco
|
id("org.scoverage") version "8.1"
|
||||||
application
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +15,11 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scala {
|
scala {
|
||||||
versions["SCALA3"]!!
|
scalaVersion = versions["SCALA3"]!!
|
||||||
|
}
|
||||||
|
|
||||||
|
scoverage {
|
||||||
|
scoverageVersion.set(versions["SCOVERAGE"]!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -31,17 +35,6 @@ tasks.named<JavaExec>("run") {
|
|||||||
standardInput = System.`in`
|
standardInput = System.`in`
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
|
||||||
finalizedBy(tasks.jacocoTestReport)
|
|
||||||
}
|
|
||||||
tasks.jacocoTestReport {
|
|
||||||
dependsOn(tasks.test)
|
|
||||||
reports {
|
|
||||||
xml.required.set(true)
|
|
||||||
html.required.set(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation("org.scala-lang:scala3-compiler_3") {
|
implementation("org.scala-lang:scala3-compiler_3") {
|
||||||
@@ -54,29 +47,17 @@ dependencies {
|
|||||||
strictly(versions["SCALA3"]!!)
|
strictly(versions["SCALA3"]!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
implementation("org.scala-lang:scala-library") {
|
|
||||||
version {
|
|
||||||
strictly(versions["SCALA_LIBRARY"]!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation(project(":modules:api"))
|
implementation(project(":modules:api"))
|
||||||
|
|
||||||
testImplementation(platform("org.junit:junit-bom:5.10.0"))
|
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.scalatestplus:junit-5-11_3:${versions["SCALATESTPLUS_JUNIT5"]!!}")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
|
||||||
testImplementation("org.scalatest:scalatest_3:3.2.19")
|
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-engine:1.13.1")
|
|
||||||
testRuntimeOnly("org.scalatestplus:junit-5-13_3:3.2.19.0")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks.test {
|
||||||
test{
|
useJUnitPlatform()
|
||||||
useJUnitPlatform {
|
finalizedBy(tasks.reportScoverage)
|
||||||
includeEngines("scalatest")
|
}
|
||||||
testLogging {
|
tasks.reportScoverage {
|
||||||
events("passed", "skipped", "failed")
|
dependsOn(tasks.test)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user