zio-bdd

A BDD testing framework integrating Gherkin-style tests with ZIO’s effect system

View the Project on GitHub EtaCassiopeia/zio-bdd

Verified Examples

These snippets are compile-checked against the current API. They are generated by mdoc from mdoc-src/verified-examples.md via sbt 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 minimal step suite

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)
  }

Assertions

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 ()

The mock DSL

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"))

Verifying a third-party adapter (conformance kit)

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.