Sign in

    moonspec

    BDD test framework for MoonBit with Gherkin and Cucumber Expressions

    bdd
    cucumber
    gherkin
    testing
    moonbit
    Download zip
    Author
    Version
    0.11.0
    License
    Apache-2.0
    Last updated
    9 days ago
    Downloads
    7K

    #moonspec

    BDD test framework for MoonBit with Gherkin and Cucumber Expressions.

    #Installation

    moon add moonrockz/moonspec

    Add moonrockz/moonspec to the import array in your moon.pkg.

    #Quick Start

    Define a World, implement step definitions, and run against inline Gherkin:

    struct CalcWorld { mut result : Int } derive(Default)

    impl @moonspec.World for CalcWorld with configure(self, setup) {
    setup.given0("a calculator", fn() { self.result = 0 })
    setup.when2("I add {int} and {int}", fn(a : Int, b : Int) {
    self.result = a + b
    })
    setup.then1("the result should be {int}", fn(n : Int) {
    assert_eq(self.result, n)
    })
    }

    async test "Feature: Calculator" {
    let feature =
    #|Feature: Calculator
    #| Scenario: Addition
    #| Given a calculator
    #| When I add 2 and 3
    #| Then the result should be 5
    @moonspec.run_or_fail(CalcWorld::default,
    @moonspec.RunOptions::new([@moonspec.FeatureSource::Text("calc", feature)]),
    )
    }

    Each scenario gets a fresh World via derive(Default). MoonBit structs are reference types -- mutations in closures are visible across step handlers.

    #Generate Tests with the CLI

    The moonspec CLI writes the wiring for you. You write .feature files and annotated step functions. The CLI then generates the World configure impl and one moon test test per scenario. Build rules keep the generated tests in step with the feature files.

    You can use the CLI in two ways. Both run the same commands.

    Installed CLImoonx (no install)
    Set upmoon install moonrockz/moonspec/cmd/moonspecNone. moonx comes with the MoonBit toolchain
    Run a commandmoonspec <command>moonx moonrockz/moonspec/cmd/moonspec@latest <command>
    Build rulesmoonspec gen build ... --apply... gen build ... --apply --moonx

    moon install puts moonspec in ~/.moon/bin. With --moonx, the build rule runs a pinned CLI version through moonx, so nobody who builds the project needs to install the CLI.

    #1. Add the dependencies

    moon add moonrockz/moonspec moon add moonbitlang/async

    In the moon.pkg of the package that holds your steps (here src):

    import { "moonrockz/moonspec", } import { "moonrockz/moonspec/format", "moonbitlang/async", } for "wbtest"

    #2. Write a feature file

    features/calculator.feature:

    Feature: Calculator Scenario: Addition Given a calculator When I add 2 and 3 Then the result should be 5

    #3. Write annotated step functions

    src/calc.mbt:

    #moonspec.world
    struct CalcWorld {
    mut result : Int
    } derive(Default)

    pub extend CalcWorld with Default::{default}

    #moonspec.given("a calculator")
    fn given_calculator(world : CalcWorld) -> Unit {
    world.result = 0
    }

    #moonspec.when("I add {int} and {int}")
    fn when_add(world : CalcWorld, a : Int, b : Int) -> Unit {
    world.result = a + b
    }

    #moonspec.then("the result should be {int}")
    fn then_result(world : CalcWorld, expected : Int) -> Unit raise Error {
    assert_eq(world.result, expected)
    }

    #4. Generate the World impl

    moonspec gen steps # or: moonx moonrockz/moonspec/cmd/moonspec@latest gen steps

    This scans src (-d sets another directory) and writes src/calcworld_steps_gen.mbt. The file implements World for CalcWorld with one registration per annotated function:

    // Generated by moonspec gen steps — DO NOT EDIT
    #warnings("-unused_value")
    impl @moonspec.World for CalcWorld with configure(self, setup) {
    setup.given0("a calculator", fn() {
    given_calculator(self)
    })
    setup.when2("I add {int} and {int}", fn(_arg0 : Int, _arg1 : Int) {
    when_add(self, _arg0, _arg1)
    })
    setup.then1("the result should be {int}", fn(_arg0 : Int) raise {
    then_result(self, _arg0)
    })
    }

    Run it again when you add or change a step function. The parameters after the world select the registration:

    Step function parametersRegistration
    (world) or (world, a : A, ...)given0, given2, ... (typed, up to 22)
    (world, a : A, ..., ctx : @moonspec.Ctx)given1_ctx, ... (typed plus Ctx, up to 22)
    (world, ctx : @moonspec.Ctx)given("...", fn(ctx) ...), a handler that reads its own arguments
    any of the above with handler=true in the attributegiven("...", fn(ctx) ...), decoding typed values with no arity check; T? is None for a missing argument

    Typed values are decoded with FromStepArg (Int, String, @moonspec.DataTable, ...). The attributes are #moonspec.given, when, then and step.

    #5. Add build rules and generate the tests

    moonspec gen build features/calculator.feature -w CalcWorld --apply # or: moonx moonrockz/moonspec/cmd/moonspec@latest gen build \ # features/calculator.feature -w CalcWorld --apply --moonx

    This adds a rule and a dev_build entry to src/moon.pkg and writes src/calculator_feature_wbtest.mbt, with one async test per scenario. From then on, moon regenerates the test file before each moon check, moon build and moon test when the feature file changes.

    #6. Run the tests

    moon test

    Each scenario is its own test, so moon test -f "Feature: Calculator / Scenario: Add*" runs a subset and moon test --outline lists them.

    Commit the generated files. moon does not run build rules when your package is a dependency of another module.

    #CLI commands

    CommandPurpose
    gen steps [-d <dir>]Generate the World configure impl from #moonspec.* attributes
    gen build <features...> -w <World> [--apply] [--moonx] [-p <pkg>] [--test-kind <kind>]Print or apply moon.pkg build rules; with --apply, also generate missing test files
    gen tests <features...> -w <World> -o <dir or file.mbt> [--test-kind <kind>]Generate test files once, without build rules
    check <features...>Parse feature files and report errors
    versionPrint the CLI version

    Generated test files are white-box tests (*_wbtest.mbt) by default, so they can use a private World impl, such as the one gen steps generates. Pass --test-kind test (or set test_kind: "test" in moonspec.json5) for black-box *_test.mbt files; these see only the package's public API. An -o value that ends in .mbt is the exact file name. A moonspec.json5 file can set world, the codegen mode and other defaults; see Config File.

    #Features

    • World trait -- per-scenario state with derive(Default)
    • StepLibrary trait -- composable, reusable step groups
    • Cucumber Expressions -- 11 built-in parameter types, custom typed handles, and capture decoders
    • Expression tools -- registry-aware expression factory and candidate generator
    • Gherkin -- Feature, Scenario, Scenario Outline, Background, Rules, Data Tables, Doc Strings
    • Markdown with Gherkin -- .feature.md files are parsed as MDG
    • Lifecycle hooks -- before/after for test run, test case, and test step
    • Tag filtering -- boolean expressions (@smoke and not @slow)
    • Retries -- @retry(N) tags or global config
    • Dry-run -- validate wiring without execution
    • Skip -- @skip / @ignore with optional reason
    • Parallel execution -- bounded concurrency via @async.all()
    • Attachments -- text, binary, or URL on steps and hooks
    • Structured errors -- run_or_fail with snippets and suggestions
    • Codegen -- generate moon test runners from .feature and .feature.md files
    • Formatters -- Pretty, Cucumber Messages (NDJSON), JUnit XML

    #Cucumber Expression Parameters

    ExpressionMoonBit TypeStepValue Variant
    {int}IntIntVal(Int)
    {float}DoubleFloatVal(Double)
    {double}DoubleDoubleVal(Double)
    {long}Int64LongVal(Int64)
    {byte}ByteByteVal(Byte)
    {short}IntShortVal(Int)
    {bigdecimal}@decimal.DecimalBigDecimalVal(@decimal.Decimal)
    {biginteger}BigIntBigIntegerVal(BigInt)
    {string}StringStringVal(String)
    {word}StringWordVal(String)
    {}StringAnonymousVal(String)

    Custom types: setup.add_param_type_strings(name, patterns, transformer?).

    #Typed parameters and expression tools

    Define custom domain values with setup.define_param_type1 or composable @moonspec.Captures decoders through setup.define_param_type_with. Retrieve values with ctx[index].get(handle):

    let color : @moonspec.ParameterType[String] = setup.define_param_type1(
    "color", ["red|blue"], fn(text) { text },
    )
    setup.given("I pick {color}", fn(ctx) raise {
    assert_eq(ctx[0].get(color), "red")
    })

    The expression factory and generator are available through the same setup, using its registered parameter types:

    let factory = setup.expression_factory()
    let expression = factory.create_expression("I pick {color}")
    let candidates = setup.expression_generator().generate_expressions("I pick red")
    // candidates[0].source() is "I pick {color}".

    The factory creates expression objects for validation and explicit matching. Anchored patterns and /.../ patterns create regex objects; other patterns create Cucumber Expressions. The generator returns candidates and parameter metadata, honoring custom types' use_for_snippets flags. Choose a candidate's source() to register a Cucumber step pattern.

    ParameterType, Captures, Captures1, ExpressionFactory, CucumberExpressionGenerator, StepExpression, GeneratedExpression, Match, and the expression/parameter error types are re-exported by @moonspec. Step registration currently accepts Cucumber Expressions, and automatic undefined-step snippets use moonspec's built-in inference. The factory and generator are exposed for users' own tools and workflows.

    See the expression tools guide for full examples.

    #Step Registration

    Register steps inside World::configure using typed arity-suffixed methods. The numeric suffix indicates how many parameters the handler takes:

    setup.given0("a calculator", fn() { self.result = 0 })
    setup.given1("a user named {string}", fn(name : String) { self.user = name })
    setup.when2("I add {int} and {int}", fn(a : Int, b : Int) { self.result = a + b })
    setup.then1("the result should be {int}", fn(n : Int) { assert_eq(self.result, n) })
    setup.step0("the system is ready", fn() { () }) // matches any keyword

    The _ctx variants provide access to the full Ctx as the last parameter:

    setup.given1_ctx("a user named {string}", fn(name : String, ctx : Ctx) {
    self.user = name
    self.feature = ctx.scenario().feature_name
    })

    Arities 0--22 are supported for all keywords (given, when, then, step). The original setup.given("pattern", fn(ctx) { ... }) form remains available for advanced use cases.

    #Ctx and StepArg

    Ctx provides indexed access to matched arguments. Each StepArg has value (typed StepValue) and raw (original text). Use struct destructuring: match ctx[0] { { value: IntVal(n), .. } => ... }.

    Other methods: ctx.value(0) returns StepValue directly, ctx.args() returns ArrayView[StepArg], ctx.scenario() returns ScenarioInfo (feature name, scenario name, tags), ctx.step() returns StepInfo (keyword, text).

    #StepLibrary

    Composable step groups via the StepLibrary trait. Returns ArrayView[StepDef]:

    struct AccountSteps { world : BankWorld }

    impl @moonspec.StepLibrary for AccountSteps with steps(self) {
    let defs : Array[@moonspec.StepDef] = [
    @moonspec.StepDef::given1("a balance of {int}", fn(n : Int) {
    self.world.balance = n
    }),
    ]
    defs[:]
    }

    // Compose libraries in World::configure:
    setup.use_library(AccountSteps::new(self))

    #Hooks

    Register lifecycle hooks on Setup. "After" variants receive HookResult:

    setup.before_test_case(fn(ctx) {
    println("Starting: " + ctx.scenario().scenario_name)
    })
    setup.after_test_case(fn(_ctx, result) {
    // result: HookResult::Passed or HookResult::Failed(Array[HookError])
    ignore(result)
    })

    All six: before/after_test_run, before/after_test_case, before/after_test_step.

    #Attachments

    All context types (Ctx, CaseHookCtx, StepHookCtx, RunHookCtx) support attachments:

    ctx.attach("log output", "text/plain")
    ctx.attach_bytes(png_bytes, "image/png", file_name="screenshot.png")
    ctx.attach_url("https://example.com/report", "text/html")

    #RunOptions

    Configure a test run with RunOptions::new(features):

    MethodDefaultDescription
    parallel(Bool)falseEnable parallel scenario execution
    max_concurrent(Int)4Max concurrent scenarios when parallel
    tag_expr(String)""Boolean tag filter expression
    scenario_name(String)""Filter scenarios by name
    retries(Int)0Global retry count for failed scenarios
    dry_run(Bool)falseValidate wiring without execution
    skip_tags(Array[String])["@skip", "@ignore"]Tags that skip scenarios
    add_sink(&MessageSink)--Add a formatter for envelope output
    add_formatter(sink, dest)--Register formatter with output destination
    clear_sinks()--Remove all sinks and formatters

    Feature sources: FeatureSource::Text(uri, content) for inline Gherkin, FeatureSource::File(path) to load from disk.

    #Formatters

    Three built-in formatters (all implement MessageSink):

    • Pretty -- @format.PrettyFormatter::new() -- colored console output
    • JUnit -- @format.JUnitFormatter::new() -- XML for CI
    • Messages -- @format.MessagesFormatter::new() -- Cucumber Messages NDJSON

    Register with a destination:

    options.add_formatter(@format.PrettyFormatter::new(), @moonspec.OutputDest::Stdout)
    options.add_formatter(@format.JUnitFormatter::new(), @moonspec.OutputDest::File("report.xml"))

    Output destinations: @moonspec.OutputDest::Stdout, @moonspec.OutputDest::Stderr, @moonspec.OutputDest::File(path).

    When no formatters are configured, defaults to pretty output on stdout.

    #Tag Filtering

    opts.tag_expr("@smoke") // only @smoke
    opts.tag_expr("@smoke and not @slow") // boolean operators
    opts.tag_expr("@smoke or @regression") // either tag

    #Retrying, Dry-Run, Skip

    Retrying -- global or per-scenario @retry(N) tag (overrides global). Each retry creates a fresh World:

    opts.retries(2) // retry failed scenarios up to 2 times

    Dry-run -- validate step wiring without execution. Matched steps report as Skipped("dry run"), undefined steps report with snippets:

    opts.dry_run(true)

    Skip -- scenarios tagged @skip or @ignore are skipped by default. Add a reason with @skip("flaky on CI"). Configure:

    opts.skip_tags(["@skip", "@ignore", "@wip"])

    #Config File

    moonspec.json5 controls codegen and runtime behavior:

    { world: "MyWorld", mode: "per-scenario", // or "per-feature", or per-file map steps: { output: "steps.mbt", exclude: ["features/wip/**"] }, skip_tags: ["@skip", "@ignore"], formatters: [ { type: "pretty", output: "stdout" }, { type: "junit", output: "reports/results.xml" } ] }

    #Packages

    PackageDescription
    moonrockz/moonspecFacade -- World, Setup, Ctx, RunOptions, run, run_or_fail
    moonrockz/moonspec/coreWorld, Setup, HookRegistry, StepRegistry, Ctx
    moonrockz/moonspec/runnerExecutor with tag filtering and parallel support
    moonrockz/moonspec/formatPretty, Messages, JUnit formatters
    moonrockz/moonspec/codegenGenerate test files from Gherkin features
    moonrockz/moonspec/configConfiguration parsing (moonspec.json5)
    moonrockz/moonspec/scanner#moonspec.* attribute scanner for gen steps
    moonrockz/moonspec/cliCLI implementation
    moonrockz/moonspec/cmd/moonspecCLI executable (moonspec)

    #Documentation

    Full docs, CLI reference, architecture, and examples: github.com/moonrockz/moonspec

    Upgrading from 0.10.x? See the 0.11.0 migration guide.

    #License

    Apache-2.0

    AmbiguousParameterTypeError

    Cucumber expression tools and typed parameter APIs available to consumers.

    Attachable

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    Captures

    Cucumber expression tools and typed parameter APIs available to consumers.

    Captures1

    Cucumber expression tools and typed parameter APIs available to consumers.

    CaseHookCtx

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    Cells

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    Column

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    Columns

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    Ctx

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    CucumberExpressionGenerator

    Cucumber expression tools and typed parameter APIs available to consumers.

    DataTable

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    DocString

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    ExpressionError

    Cucumber expression tools and typed parameter APIs available to consumers.

    ExpressionFactory

    Cucumber expression tools and typed parameter APIs available to consumers.

    FeatureResult

    Result of executing a feature.

    FeatureSource

    Input source for a feature to be loaded into the cache.

    FromStepArg

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    GeneratedExpression

    Cucumber expression tools and typed parameter APIs available to consumers.

    HookError

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    HookHandler

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    HookRegistry

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    HookResult

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    HookType

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    Match

    Cucumber expression tools and typed parameter APIs available to consumers.

    MoonspecError

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    OutputDest

    Destination for formatter output.

    ParameterType

    Cucumber expression tools and typed parameter APIs available to consumers.

    ParameterTypeError

    Cucumber expression tools and typed parameter APIs available to consumers.

    PendingAttachment

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    RegisteredHook

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    Row

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    Rows

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    RunHookCtx

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    RunOptions

    Options for configuring a test run.

    RunResult

    Complete result of a test run.

    RunSummary

    Summary of an entire run.

    The retried field counts scenarios that required more than one attempt, regardless of their final outcome. A scenario that fails on the first attempt but passes on retry is counted as both passed and retried.

    ScenarioInfo

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    ScenarioResult

    Result of executing a scenario.

    ScenarioStatus

    Aggregate status of a scenario.

    Setup

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepArg

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepDef

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepExpression

    Cucumber expression tools and typed parameter APIs available to consumers.

    StepHookCtx

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepInfo

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepKeyword

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepLibrary

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepMatchResult

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepRegistry

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepReport

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepResult

    Result of executing a single step.

    StepSource

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    StepStatus

    Status of a single step execution.

    StepValue

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    World

    moonspec — BDD test framework for MoonBit.

    Re-exports core types and runner functions so users import just moonrockz/moonspec and use @moonspec.World, @moonspec.run, etc.

    color_enabled

    fn color_enabled(no_color? : Bool) -> Bool

    Decide whether moonspec output uses ANSI colors.

    no_color is the value compiled into the test (from RunOptions, moonspec.json5 or moonspec gen tests --no-color). Environment variables override it at runtime:

    • MOONSPEC_COLOR=always or MOONSPEC_COLOR=never forces color on or off.
    • NO_COLOR with any non-empty value turns color off (https://no-color.org).

    run

    Run all features and collect results.

    A fresh world is created per scenario via factory() for isolation. When parallel is greater than 0, pickles are executed concurrently using @async.all() with bounded concurrency. Otherwise, pickles run sequentially. Lifecycle hooks registered via Setup are called automatically when present.

    run_or_fail

    async fn[W :
    World
    ] run_or_fail(factory : () -> W, options :
    RunOptions
    ) -> Unit

    Run all features, raising MoonspecError on any failure.

    This is the ergonomic test API. Use in generated tests and manual tests where you want structured error output instead of manual result inspection. The raised RunFailed prints a cucumber-style report of every parse error and failed scenario. It uses ANSI colors unless RunOptions::no_color is set, or the environment overrides it (see @core.color_enabled).

    Source Files