-
Notifications
You must be signed in to change notification settings - Fork 19
Gherkin DSL for Swift
Sometimes there's a preference to keep everything in Swift instead of trying to point to .feature
files. This has some great advantages in terms of flexibility and friendliness for developers. It also has some tradeoffs. It's important to remember that the value of Gherkin is largely its readability. To that end CucumberSwift's DSL is designed to help drive that readability.
Ostensibly you should put your feature declarations inside the setupSteps
method of Cucumber. Example:
extension Cucumber: StepImplementation {
func setupSteps() {
//define features here
}
}
NOTE: these features also obey the optional
shouldRunWith
method. Including the ability to specify lines you want run. You can treat this largely the same as using.feature
files.
I suggest you make use of the new [self]
closure semantics to avoid having to either sprinkle self.
everywhere or having to come up with ways to avoid having to put self.
everywhere.
Example:
Scenario("") { [self] in
Given(some: precondition()) //no self.precondition()
}
Steps in the DSL are defined using an @autoclosure
. It's important to understand this means you can provide a single void function, but not a closure. This is on purpose because idiomatic Gherkin is 1 line per step.
Example:
func userExists(withName: String) {
//logic
}
//Step definition:
Given(a: userExists(withName: "John Doe"))
Like other forms of Gherkin a Step
must be embedded in a Scenario
which in turn is embedded in a Feature
. If you define a step and do not add it to a Scenario
that is part of a Feature
it will not execute. This is once again to help drive towards idiomatic Gherkin.
Scenarios have a title, optional tags, and a series of steps. They use a function builder (much like SwiftUI) to give a friendly API. That comes with 2 important caveats. The first is you must supply some steps in the closure, you cannot define an empty scenario. The second is that function builders are relatively new. This means that error handling around them isn't very friendly yet. If you find yourself seeing an abort trap 6
or segmentation fault
error try extracting that code out of the scenario closure, and seeing if you can get it working in isolation, then add it back.
Example:
Scenario("title") {
Given(I: print("Hello World!"))
}
//with tags
Scenario("title", tags: ["tag1", "tag2"]) {
Given(I: print("Hello World!"))
}
NOTE: Once again be aware that a
Scenario
must be defined as part of aFeature
if it is going to execute.
Scenario Outlines are special type that generate multiple Scenario
s. They always have an Examples
array attached to them that is used to create 1 Scenario
per example.
Example:
ScenarioOutline("SomeTitle",
headers: (first:String, last:String, balance:Double).self,
steps: { (first, last, balance) in
Given(a: personNamed(first, last))
When(I: searchFor(last))
Then(I: seeABalanceOf(balance))
}, examples: {
[
(first: "John", last: "Doe", balance: 0),
(first: "Jane", last: "Doe", balance: 10.50),
]
})
The headers
property is where you describe a type. I recommend a tuple, like the example. This reads somewhat similarly to Gherkin you'd find in a .feature
file. The examples
closure must return an array of that type. Finally the steps
closure is called once per example, with the data in that example.
Some ScenarioOutline
s should have a title that changes per example
too. To make that happen there is a title closure you can use.
NOTE: As you're writing a
ScenarioOutline
with a title closure the compiler won't be able to determine the type right away. Finish defining theScenarioOutline
before trying to fix any errors it throws.
Example:
ScenarioOutline("\($0.first)'s balance is accurate",
headers: (first:String, last:String, balance:Double).self,
steps: { (first, last, balance) in
Given(a: personNamed(first, last))
When(I: searchFor(last))
Then(I: seeABalanceOf(balance))
}, examples: {
[
(first: "John", last: "Doe", balance: 0),
(first: "Jane", last: "Doe", balance: 10.50),
]
})
Features are the top-level object for all Gherkin. Features have a title, optional tags, and a series of Scenarios. They use a function builder (much like SwiftUI) to give a friendly API. That comes with 2 important caveats. The first is you must supply some Scenarios in the closure, you cannot define an empty Feature
. The second is that function builders are relatively new. This means that error handling around them isn't very friendly yet. If you find yourself seeing an abort trap 6
or segmentation fault
error try extracting that code out of the Feature
closure, and seeing if you can get it working in isolation, then add it back.
Example:
Feature("title") {
Scenario("scnTitle") {
Given(I: print("Hello World!"))
}
}
//with tags
Feature("title", tags: ["t1", "t2"]) {
Scenario("scnTitle") {
Given(I: print("Hello World!"))
}
}
Backgrounds are a special kind of Scenario
without a title, that add all of their steps to the beginning of all other scenarios in a Feature
Example:
Feature("F1") {
Background {
Given(I: print("B1"))
}
Scenario("SC1") {
//will execute Given(I: print("B1")) first
When(I: print("S1"))
}
}
Descriptions are a special kind of Scenario
that does not require steps. It's purely there to add context.
Example:
Feature("Some terse yet descriptive text of what is desired") {
Description("""
Textual description of the business value of this feature
Business rules that govern the scope of the feature
Any additional information that will make the feature easier to understand
""")
Scenario("scnTitle") {
Given(I: print("Hello World!"))
}
}
Rules are a Gherkin v6 feature find out more. This document won't posit on their value or their readability, instead it's going to focus on their functionality. They allow you group together Scenario
s and have multiple levels of Background
steps.
Example:
Feature("F1") {
Background {
Given(I: print("B1"))
}
Rule("R1") {
Background {
Given(I: print("B2"))
}
Scenario("SC1") {
//First it executes: Given(I: print("B1"))
//Then it executes: Given(I: print("B2"))
Given(I: print("S1"))
}
}
Scenario("SC2") {
//First it executes: Given(I: print("B1"))
Given(I: print("S2"))
}
}
Here's an example of how this works altogether.
extension Cucumber: StepImplementation {
public var bundle: Bundle {
class This { }
return Bundle(for: This.self)
}
public func shouldRunWith(scenario: Scenario?, tags: [String]) -> Bool {
return tags.contains("t1")
}
func setupHooks() { //NOTE: These hooks also work with the DSL
BeforeFeature { feature in }
BeforeScenario { scenario in }
BeforeStep { step in }
AfterStep { step in }
AfterScenario { scenario in }
AfterFeature { feature in }
}
public func setupSteps() {
setupHooks()
Feature("Some terse yet descriptive text of what is desired") {
Description("""
Textual description of the business value of this feature
Business rules that govern the scope of the feature
Any additional information that will make the feature easier to understand
""")
Background {
Given(I: print("This runs before the steps in all scenarios"))
}
Scenario("Some determinable business situation", tags:["t1"]) {
Given(some: precondition())
When(some: actionByTheActor())
Then(some: testableOutcomeIs(.achieved))
}
ScenarioOutline({ "Before \($0) hook works correctly" }, headers: String.self, steps: { scn in
Given(I: haveABeforeScenarioHook())
When(I: runTheTests())
Then(beforeScenarioGetsCalledOnScenario(withTitle: "Before \(scn) hook works correctly"))
}, examples: {
[
"scenario",
"scenario outline"
]
})
}
}
}
Localization is not commonly supported when creating APIs. However because Cucumber ships with a JSON file that describes different supported languages and their mappings to specific keywords this became a possibility. Non-English Gherkin is prefixed by its language code BG_Пример
won't read quite as well as Пример
but it was necessary to avoid a truly shocking amount of conflicts.
While running by line is supported trying to use that feature to run a specific example in a ScenarioOutline
isn't particularly reasonable. Examples could be defined inline, or maybe they're all defined on the same line, or maybe they're extracted into a different file altogether. Because this extreme flexibility is possible in the DSL running a specific example by line is only supported when using .feature
files.
After a lot of thought if a DataTable is desired on a step the implementor can handle that. While we could take an approach similar to ScenarioOutline
it'd require giving up on things like the @autoclosure
in steps. These things are in place specifically to help guide people towards writing idiomatic Gherkin in Swift. DataTables do not seem like they're a useful enough feature to warrant that.
Swift supports multi-line strings which are very similar to DocStrings. The difference is that DocStrings in Gherkin can have a type,
Example:
"""<xml>
<node>thing</node>
"""
Similar to the discussion on DataTables, this just doesn't seem like it's really worth a whole customized part of the DSL. If you'd like to use multi-line strings that is already supported, if those has a specific type associated with them, that seems like something the implementor can handle easily.