Troubleshooting
Common issues encountered when writing Cornichon tests and how to fix them.
Placeholder not resolved
Symptom: A step receives a literal <my-key> string instead of the expected value.
Cause: The key does not exist in the session at the time the placeholder is resolved.
Fix: Make sure a previous step saves a value under that key. Common sources:
- A
save_body_pathstep is missing or uses a different key name - The placeholder is used before the step that populates it
- A typo in the key name — placeholders are case-sensitive
// Wrong: key was never saved
When I get("/users/<user-id>")
// Right: save the key first
And I save_body_path("id" -> "user-id")
When I get("/users/<user-id>")
Using body on a non-JSON response
Symptom: An error like expected JSON but got ... or a parsing failure.
Cause: The body assertion expects JSON content. If the endpoint returns plain text, XML, or HTML, parsing fails.
Fix: Use body_raw for non-JSON responses:
// Wrong: endpoint returns plain text
Then assert body.is("OK")
// Right
Then assert body_raw.is("OK")
Then assert body_raw.containsString("success")
Eventually block times out
Symptom: Eventually block did not complete in time after having being tried 'N' times
Causes and fixes:
- Timeout too short: Increase
maxDuration. Eventually-consistent systems can have variable delays. - Interval too long: Reduce
intervalto check more frequently. - Assertion never holds: The system under test may not reach the expected state. Check application logs.
- Wrong assertion: Verify your assertion would pass if run manually after the system settles.
// Too aggressive — might miss the window
Eventually(maxDuration = 1.second, interval = 500.millis) { ... }
// More realistic
Eventually(maxDuration = 15.seconds, interval = 200.millis) { ... }
Scenarios interfere when run in parallel
Symptom: Tests pass individually but fail when run together. Flaky failures related to shared data.
Cause: By default, scenarios within a feature run in parallel. If scenarios share mutable state (database records, server state), they can interfere.
Fixes:
- Make scenarios independent: Use unique identifiers (e.g.,
<random-uuid>) for test data. - Run sequentially: Set
executeScenariosInParallel = falseinapplication.conf— see Reference Configuration. - Use setup/teardown: Clean up shared state in
beforeEachScenario/afterEachScenario.
// Use random data to avoid collisions
Given I post("/products").withBody(
"""{ "name": "test-<random-uuid>", "price": 42 }""")
Session key overwritten unexpectedly
Symptom: A placeholder resolves to a different value than expected.
Cause: The session behaves like a multimap — saving the same key multiple times appends values, and <key> always returns the latest. Inside loops (Repeat, RepeatWith), each iteration may overwrite the key.
Fixes:
- Use
<key[0]>,<key[1]>etc. to access earlier values by index - Use
session_history("key").containsExactly(...)to verify the full history - Use distinct key names when saving multiple values
// Inside a Repeat, each iteration overwrites "response-status"
Repeat(3) {
When I get("/endpoint")
// "last-status" will always be the latest value
}
// Use indexed access for earlier values
Then assert session_value("response-status").atIndex(0).is("200")
Repeat index starts at 0
Symptom: Off-by-one errors when using Repeat with an index.
Cause: The iteration index is zero-based.
Repeat(3, "i") {
// i takes values: "0", "1", "2"
When I get("/items/<i>")
}
JSON path returns unexpected value
Symptom: A body.path(...) assertion fails even though the response looks correct.
Common causes:
- Array vs object:
body.path("items")returns the raw JSON array. Usebody.path("items").asArrayfor array-specific assertions. - Nested path with dots in keys: If a key contains a
., wrap it with backticks:body.path("`field.name`") - Root array access: For a response that is a JSON array, use
$as the root:body.path("$[0].name") - Wildcard projection:
body.path("items[*].name")returns an array of allnamevalues.
Status code assertion fails with response details
Symptom:
expected status code '200' but '401' was received with body:
"Unauthorized"
Fix: Cornichon includes the response body and headers in status failures. Read them carefully — they often explain why the server rejected the request (expired token, missing header, wrong content type, etc.).
Feature not discovered by test framework
Symptom: SBT runs but reports 0 tests.
Cause: The test framework is not registered, or the feature class is not in src/test/scala.
Fix: Verify your build.sbt includes:
testFrameworks += new TestFramework("com.github.agourlay.cornichon.framework.CornichonFramework")
And that your feature class extends CornichonFeature and is in the src/test/scala directory.
Debugging tips
When a test fails and the cause isn't obvious:
-
Enable request tracing in
application.confto see full HTTP request/response details:cornichon { traceRequests = true } -
Use debug steps to inspect state mid-scenario:
And I show_session And I show_last_status And I show_last_body_json And I show_last_headers -
Replay with a fixed seed using the command from the failure output to reproduce the exact same execution — see Understanding Test Output.
-
Focus on one scenario during debugging to isolate the issue:
Scenario("the failing one").focused { ... }See Feature Options for details.