A BDD testing framework integrating Gherkin-style tests with ZIO’s effect system
These snippets are compile-checked against the current API. They are generated by
mdocfrommdoc-src/verified-examples.mdviasbt docs/mdoc, and CI fails if any of them stops compiling — so this page can’t silently drift the way hand-written examples can. The rest of the docs use plain Markdown code blocks (mostly illustrative fragments), which are not compiled; treat this page as the source of truth for canonical, copy-pasteable code.
A ZIOSteps[R, S] object wires Gherkin steps to effects. R is the ZIO environment, S the
per-scenario state (a case class with a Schema, so its Default is derived automatically). The
*S step variants receive the current state; ScenarioContext.update mutates it.
import zio.*
import zio.bdd.core.Assertions.assertEquals
import zio.bdd.core.Suite
import zio.bdd.core.step.ZIOSteps
import zio.schema.{DeriveSchema, Schema}
case class CounterState(count: Int = 0)
object CounterState:
given Schema[CounterState] = DeriveSchema.gen[CounterState]
@Suite(featureDirs = Array("src/test/resources/features"))
object CounterSteps extends ZIOSteps[Any, CounterState]:
Given("a counter starting at zero") { ZIO.unit }
WhenS("it is incremented by" / int) { s => n =>
ScenarioContext.update(c => c.copy(count = c.count + n))
}
ThenS("the counter equals" / int) { s => expected =>
assertEquals(s.count, expected)
}
zio-bdd’s own assertions live in zio.bdd.core.Assertions; each returns an effect that fails the
step on a false condition.
import zio.*
import zio.bdd.core.Assertions.*
val checks =
for
_ <- assertEquals(2 + 2, 4)
_ <- assertTrue(List(1, 2, 3).nonEmpty)
yield ()
zio.bdd.mock.dsl builds portable request/response rules for the MockControl SPI.
import zio.bdd.mock.dsl.*
val healthStub = get("/health").respondWith(ok.text("OK"))
An adapter written outside this repo can run the official conformance suite — the same one that
gates the bundled Rift and WireMock adapters — from the published zio-bdd-mock-conformance
artifact. Register the adapter as a MockBackendUnderTest with the capabilities it advertises, run
the portable scenario sets through ConformanceHarness, and assert the column is conformant.
(myAdapterLayer stands in for your adapter’s own MockControl layer.)
import zio.*
import zio.bdd.mock.*
import zio.bdd.mock.conformance.*
import zio.test.*
object MyAdapterConformanceSpec extends ZIOSpecDefault:
val myAdapterLayer: ZLayer[Any, Throwable, MockControl] = ???
val backend = MockBackendUnderTest(
name = "my-adapter",
layer = myAdapterLayer,
capabilities = Set(Capability.Faults), // what the adapter advertises
isolation = Isolation.PerInstance
)
val scenarios: List[ConformanceScenario] =
CoreConformanceScenarios.all ++
NegotiationErrorScenarios.all ++
FaultScenarios.all ++
ScriptingScenarios.all ++
TemplatingScenarios.all ++
CapStatefulScenarios.all
def spec = suite("MyAdapter conformance")(
test("the official conformance column is green") {
for
matrix <- ConformanceHarness.run(List(backend), scenarios)
_ <- ZIO.logInfo("conformance matrix:\n" + matrix.render)
yield assertTrue(matrix.conformant(backend))
}
)
A scenario that requires a capability the backend does not advertise is SKIP, never FAIL —
a negotiated gap is not a conformance breach. matrix.conformant(backend) holds as long as no cell
fails and every skip is justified by the advertised capability set, the same acceptance rule the
bundled adapters are held to.