CucumberSwift (no, not that one)

A while back, I wanted to create a Cucumber implementation for Swift that "feels like SwiftUI, but for testing".

Haven't touched the project in a while because I had run into a wall, but I recently found out that this wall has been removed, so I continued working. Even though this is just a hobby project that might help me test some other hobby projects, I found the API neat enough that I hope it inspires other devs.

Here is the result (not to be confused with a popular package with the same name that's been around for a while as I found out).

Some noteworthy neat features:

  • conformance to the Step protocol can be generated by the "GenSteps" plugin from a simple yaml specification that is compatible with the vscode extension Cucumber (Gherkin) Full Support - this means vscode has something to validate your .feature files against
  • regex and number of arguments passed to the step handler are automatically compatible
  • access to scenario-scoped state via property wrappers

To give you a minimal impression of what state management and accessing arguments looks like, here's how you define scenario-scoped properties:

extension StateContainer {
    @ContainerStorage var cucumber : Cucumber?
    @ContainerStorage var file : URL?
    @ContainerStorage var result : CukeResult?
}

Here's how you define steps:

# groupName is the name of an array into which all
# step definitions here will be dumped.
# if you use the same groupName in different yaml files,
# all steps will go into the same array
groupName: globSteps
steps:
    - step: "^a\/an (unimplemented|pending|flawed|implemented) cucumber$"
      className: GivenCucumber
      arguments:
        - name: cukeState
          type: CukeState
...
types:
    - name: CukeState
      kind: enum
      cases:
       - unimplemented
       - pending
       - flawed
       - implemented
# there's also support for enum types defined in Swift and some basic types like String, Int and Float

And here's how you use it:

// no protocol conformance necessary, this is done
// in a generated file
struct GivenCucumber {
    
    @Scenario(\.cucumber) var cucumber : Cucumber?
    
    // notice the usage of the CukeState enum type
    func onRecognize(cukeState: CukeState) async throws {
        // you can initialize cucumber for all subsequent steps
        // and use it like a normal property
        // if you're sure an earlier step set it, use @Required
        // this can for example be useful for a when/then pair
    }
    
}
1 Like