一键导入
kotest
Kotest BehaviorSpec/MockK-mønstre for sokos-ske-krav: scenariostruktur, DBListener/SftpListener, MockHttpClient, circuit-breaker-reset og matchers
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Kotest BehaviorSpec/MockK-mønstre for sokos-ske-krav: scenariostruktur, DBListener/SftpListener, MockHttpClient, circuit-breaker-reset og matchers
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Flyway-migrasjoner for PostgreSQL: navnekonvensjoner (V{n}__desc.sql), skjema for krav/feilmelding/filvalideringsfeil, DBListener-testing med TestContainers
Post-send livssyklus: StatusService poller /mottaksstatus; StoppKravService; RapportService + AvstemmingRouting-frontend for manuell reskontroføring
HOCON-basert konfigurasjon med PropertiesConfig singleton for Ktor batch-tjenester på NAIS (FSS)
SlackService feilakkumulering og Tags-basert varsling for prosesseringsfeil i SKE-batch
Endring av eksisterende krav (endre rente og endre hovedstol): EndreKravService, request-DTOer, status-konformering og SKE API PUT-endepunkter
Idiomatic Kotlin: null safety, immutability, sealed types, structured concurrency, extension functions, DSL builders, Gradle Kotlin DSL
| name | kotest |
| description | Kotest BehaviorSpec/MockK-mønstre for sokos-ske-krav: scenariostruktur, DBListener/SftpListener, MockHttpClient, circuit-breaker-reset og matchers |
Før du konkluderer med at et område mangler tester, list alltid alle testfiler:
find src/test -type f -name "*.kt" | sort
Testene i dette prosjektet er organisert i underkategorier — bruk aldri | head som kapper output.
Forventet mappestruktur:
src/test/.../service/
integration/ ← *IntegrationTest.kt (DBListener + SftpListener)
unit/ ← *Test.kt (MockK, ingen DB)
src/test/.../database/ ← RepositoryTest*.kt
src/test/.../validation/ ← *Test.kt + *IntegrationTest.kt
src/test/.../domain/ ← parser- og domene-tester
src/test/.../client/ ← SkeClientTest.kt
src/test/.../config/ ← CircuitBreaker*Test.kt
Ikke anta manglende dekning uten å ha sett full filliste først.
Default spec style: BehaviorSpec (Given/When/Then/And) in Norwegian. Use FunSpec only for trivial, non-scenario-based unit tests.
class OpprettKravServiceTest : BehaviorSpec({
val databaseServiceMock = mockk<DatabaseService> {
justRun { updateSentKrav(any<List<RequestResult>>()) }
}
Given("et nytt krav som skal sendes") {
val krav = mockk<Krav>(relaxed = true) {
every { corrId } returns "test-corr-id"
every { kravkode } returns "BA OR"
}
When("SKE svarer med 200 OK") {
val skeClientMock = mockk<SkeClient> {
coEvery { opprettKrav(any(), any()) } returns
mockHttpResponse(body = MockResponsesBody.nyttKravResponse("123"))
}
val service = OpprettKravService(skeClientMock, databaseServiceMock)
val results = service.sendAllOpprettKrav(listOf(krav))
Then("skal det returneres ett RequestResult med status KRAV_SENDT") {
results.shouldHaveSize(1)
results.first().status shouldBe Status.KRAV_SENDT
results.first().kravidentifikator shouldBe "123"
}
And("DatabaseService skal oppdatere status") {
verify(exactly = 1) { databaseServiceMock.updateSentKrav(any<List<RequestResult>>()) }
}
}
}
})
Conventions:
Given, scenario-specific mocks in When, shared fixtures on spec top-levelWhen per causal step; multiple Then/And for independent assertionsrunBlocking inside test blocks — use coEvery / coVerifyDBListener)internal class OpprettKravServiceIntegrationTest : BehaviorSpec({
extensions(DBListener)
beforeEach { CircuitBreakerManager.circuitBreaker.reset() }
val dbService = DatabaseService(DBListener.dataSource)
Given("2 nye krav som ikke er sendt") {
DBListener.clearDB()
DBListener.loadInitScript("SQLscript/krav/ToNyeKrav.sql")
val krav = dbService.getAllUnsentKrav()
krav.shouldHaveSize(2)
When("SKE svarer med OK") {
val httpClient = MockHttpClient.client(
MockResponse(Endpoint.OPPRETT, nyttKravResponse("4321"), HttpStatusCode.OK),
)
val skeClient = SkeClient("", httpClient, mockk(relaxed = true))
val results = OpprettKravService(skeClient, dbService).sendAllOpprettKrav(krav)
Then("alle krav skal være sendt") {
results.shouldHaveSize(2)
dbService.getAllUnsentKrav().shouldBeEmpty()
}
}
}
})
| DBListener helper | Purpose |
|---|---|
extensions(DBListener) | Registers TestContainers PostgreSQL 16 |
DBListener.dataSource | HikariDataSource with Flyway migrations applied |
DBListener.clearDB() | TRUNCATE ... RESTART IDENTITY CASCADE (also runs afterSpec) |
DBListener.loadInitScript("SQLscript/...") | Loads fixture from src/test/resources/ |
Always clearDB() before loadInitScript(...) inside each Given — listener state leaks between scenarios otherwise.
internal class FtpServiceIntegrationTest : BehaviorSpec({
extensions(SftpListener)
// ...
})
MockHttpClient.client(vararg MockResponse) matches by endpoint path and installs CircuitBreakerPlugin — so tests exercise production wiring.
val client = MockHttpClient.client(
MockResponse(Endpoint.OPPRETT, nyttKravResponse("id-123"), HttpStatusCode.OK),
MockResponse(Endpoint.MOTTAKSSTATUS, mottaksStatusResponse(), HttpStatusCode.OK),
)
For unit tests that don't need HTTP wiring, mock SkeClient directly:
val skeClientMock = mockk<SkeClient> {
coEvery { opprettKrav(any(), any()) } returns mockHttpResponse(body = nyttKravResponse("123"))
}
| Need | Pattern |
|---|---|
| Plain mock | mockk<T>() |
| Relaxed (auto-stub everything) | mockk<T>(relaxed = true) |
| Spy on real instance | spyk(MyService(...), recordPrivateCalls = true) |
| Suspend stub | coEvery { ... } returns ... |
Unit-returning stub | justRun { ... } / coJustRun { ... } |
| Verify | verify(exactly = n) { ... } / coVerify { ... } |
| Private function stub | every { spy["privateFun"](any<T>()) } returns ... (needs recordPrivateCalls = true) |
Prefer Kotest matchers. Group related field checks with with { ... }:
result shouldBe expected
list.shouldHaveSize(2)
list.shouldBeEmpty()
list.shouldContainExactly(a, b)
exception.message shouldContain "fagsystemId"
with(result.first()) {
httpStatusCode shouldBe HttpStatusCode.OK
status shouldBe Status.KRAV_SENDT
kravidentifikator shouldBe "123"
}
Every test that eventually calls SkeClient must reset the breaker — otherwise state leaks between tests:
beforeEach { CircuitBreakerManager.circuitBreaker.reset() }
When an open breaker is expected (e.g. multiple Forbidden responses), verify with coVerify(exactly = 1) { ... } that further calls were suppressed.
BehaviorSpec as default; Norwegian scenario textbeforeEach for SKE-reaching testsDBListener.clearDB() before every loadInitScript(...)MockHttpClient.client(...) for HTTPshouldBe, shouldHaveSize, …)coEvery / coVerify for suspend functionsrunBlocking { ... } in test blocksPropertiesConfig.load() in a test (DBListener already did it)