-
Notifications
You must be signed in to change notification settings - Fork 21
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
shouldRunWithmethod. Including the ability to specify lines you want run. You can treat this largely the same as using.featurefiles.
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
Scenariomust be defined as part of aFeatureif it is going to execute.
Scenario Outlines are special type that generate multiple Scenarios. 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 ScenarioOutlines 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
ScenarioOutlinewith a title closure the compiler won't be able to determine the type right away. Finish defining theScenarioOutlinebefore 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 Scenarios 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.