Testing
ReadingBat provides a dedicated testing module (readingbat-kotest) with utilities
for writing integration tests using Kotest.
Test Module
The readingbat-kotest module provides the TestSupport object with DSL helpers
for iterating over content, checking answers, and setting up test applications.
Setting Up Test Content
Define test content using the same DSL as production content:
val testContent =
readingBatContent {
repo = FileSystemSource("./")
java {
srcPath = "src/test/java"
group("Test Cases") {
packageName = "com.readingbat.testcontent"
challenge("StringArrayTest1")
}
}
python {
repo = FileSystemSource("../")
srcPath = "python"
group("Test Cases") {
packageName = "testcontent"
challenge("boolean_array_test") { returnType = BooleanType }
challenge("int_array_test") { returnType = IntType }
}
}
kotlin {
srcPath = "src/test/kotlin"
group("Test Cases") {
packageName = "com.readingbat.testcontent"
challenge("StringArrayKtTest1") { returnType = StringArrayType }
}
}
}
Initializing Test Properties
Before running tests, initialize the test property system:
This sets IS_PRODUCTION to false, IS_TESTING to true, and marks
the property system as initialized.
Iterating Over Content
The TestSupport object provides extension functions for iterating through
the content hierarchy:
fun iterateContentExample() {
initTestProperties()
val content = testContent
// Iterate over all languages, groups, and challenges
content.forEachLanguage {
println("Language: $languageName")
forEachGroup {
println(" Group: $groupName")
forEachChallenge {
println(" Challenge: $challengeName")
val info = functionInfo()
println(" Questions: ${info.questionCount}")
println(" Correct answers: ${info.correctAnswers}")
}
}
}
}
The iteration functions follow the DSL hierarchy:
| Function | Receiver | Iterates Over |
|---|---|---|
forEachLanguage |
ReadingBatContent |
All language groups |
forEachGroup |
LanguageGroup |
All challenge groups in a language |
forEachChallenge |
ChallengeGroup |
All challenges in a group |
forEachFuncInfo |
ChallengeGroup |
Function info for each challenge |
Accessing Specific Challenges
Use convenience functions to access specific groups or challenges directly:
fun accessChallengesExample() {
initTestProperties()
val content = testContent
// Access a specific Java group
val javaGroup = content.javaGroup("Test Cases")
println("Java group has ${javaGroup.challenges.size} challenges")
// Access a specific challenge and its function info
content.javaChallenge("Test Cases", "StringArrayTest1") {
println("Invocations: $invocations")
println("Return type: $returnType")
println("Correct answers: $correctAnswers")
}
// Access Python challenge
content.pythonChallenge("Test Cases", "boolean_array_test") {
println("Question count: $questionCount")
}
// Access Kotlin challenge
content.kotlinChallenge("Test Cases", "StringArrayKtTest1") {
println("Question count: $questionCount")
}
}
Available Accessors
| Function | Returns |
|---|---|
javaGroup(name) |
ChallengeGroup<JavaChallenge> |
pythonGroup(name) |
ChallengeGroup<PythonChallenge> |
kotlinGroup(name) |
ChallengeGroup<KotlinChallenge> |
javaChallenge(group, name) |
Applies block to FunctionInfo |
pythonChallenge(group, name) |
Applies block to FunctionInfo |
kotlinChallenge(group, name) |
Applies block to FunctionInfo |
Answer Checking
Check Individual Answers
val funcInfo = content.javaGroup("Warmup-1").functionInfo("SleepIn")
// Check a specific answer
val result = funcInfo.checkAnswer(index = 0, userResponse = "true")
result.shouldBeCorrect()
// Get a ChallengeAnswer for assertion-style checks
funcInfo.answerFor(0) shouldHaveAnswer true
funcInfo.answerFor(1) shouldNotHaveAnswer true
Check All Answers via HTTP
For full integration testing against the HTTP endpoint:
// Submit the same answer for all invocations
challenge.answerAllWith(engine, "true") {
// 'this' is a ChallengeResult
println("Status: $answerStatus, Hint: $hint")
}
// Submit the correct answer for each invocation
challenge.answerAllWithCorrectAnswer(engine) {
answerStatus shouldBe CORRECT
}
Custom Matchers
| Matcher | Description |
|---|---|
shouldBeCorrect() |
Asserts the answer result is correct |
shouldBeIncorrect() |
Asserts the answer result is incorrect |
shouldHaveAnswer(value) |
Asserts a ChallengeAnswer matches the given value |
shouldNotHaveAnswer(value) |
Asserts a ChallengeAnswer does not match |
Test Application Setup
For HTTP-level integration tests, use testModule() to set up a Ktor test application:
class MyIntegrationTest : StringSpec() {
init {
"test challenge endpoint" {
testApplication {
application {
testModule(testContent)
}
// Use the test client to make requests
val response = client.get("/content/java/Test Cases/StringArrayTest1")
response.status shouldBe HttpStatusCode.OK
}
}
}
}
The testModule() function installs:
- All Ktor features (sessions, authentication, content negotiation)
- Admin, user, and system admin routes
- WebSocket routes
- Static resource serving
Test Database
The TestDatabase object provides a Testcontainers-based PostgreSQL setup:
// Start a PostgreSQL test container and run Flyway migrations
val dataSource = TestDatabase.connectAndMigrate()
This uses a PostgreSQL 16 Alpine container and applies all migration scripts
from src/main/resources/db/migration/.