Could a BDD framework be implemented simply as a fold over a sequence of lines in a document?
Or in the words of Apple:
This changes everything. Again
Or in the words of Gojko Azdic:
Like anything else in F#, the primary use of this feature is to send it to 10 competitors whose brains you want to melt.
Let inception commence.
Fold
As defined on Wikipedia the Fold (higher-order function):
In functional programming, fold, also known variously as reduce, accumulate, compress or inject, is a family of higher-order functions that iterate an arbitrary function over a data structure in some order and build up a return value.
Or more succinctly defined via the F# List.fold signature on MSDN:
- ('State -> 'T -> 'State) -> 'State -> 'T list –> 'State
Fold high-order function implementation examples in F# and C#:
F#
|
C# |
let rec fold f acc = function
| [] -> acc
| x :: xs -> fold f (f acc x) xs
|
public static TAcc Fold<TItem,TAcc>
(Func<TAcc, TItem, TAcc> f,
TAcc acc,
IEnumerable<TItem> items)
{
foreach (var item in items)
acc = f(acc, item);
return acc;
}
|
Thusly a BDD framework can be thought of simply as a fold that iterates over a document passing the lines or steps to an arbitrary matching function.
Signature: fold: (‘context –> step –> ‘context) –> step list –> ‘context
With the accumulator value threaded between calls to the matching function representing the context, which can be explicitly scoped to the scenario under test!
Pattern Match
Given the following Feature document “Addition.txt”:
And an immutable calculator type:
type Calculator = { Values : int list } with
member this.Push n = {Values=n::this.Values}
member this.Add() = {Values=[List.sum this.Values]}
member this.Top = List.head this.Values
When folding over the lines in each scenario of the feature:
let feature = parse "Addition.txt"
feature.Scenarios
|> Seq.iter (fun scenario ->
scenario.Steps
|> List.fold performStep (Calculator())
|> ignore
)
And the following matching function is defined:
let performStep (calc:Calculator) (step,line:LineSource) =
match step with
| Given "I have entered (.*) into the calculator" [Int n] ->
calc.Push n
| When "I press add" [] ->
calc.Add ()
| Then "the result should be (.*) on the screen" [Int n] ->
Assert.AreEqual(n,calc.Top)
calc
| _ -> sprintf "Unmatched line %d" line.Number |> invalidOp
Then the feature test succeeds!
Parser
Parsing of the feature file is achieved using TickSpec’s parser module. TickSpec is an Open Source BDD framework written in F#. Underneath TickSpec’s parser is also implemented as a fold (well technically actually a scan) over a Gherkin feature document using regular expressions and Active Patterns.
To support the pattern matching function above the following Active Patterns are required:
[<AutoOpen>]
module Patterns =
open System.Text.RegularExpressions
let Regex input pattern =
let r = Regex.Match(input,pattern)
if r.Success then
Some [for i = 1 to r.Groups.Count-1 do yield r.Groups.[i].Value]
else None
let (|Given|_|) (pattern:string) (step) =
match step with
| GivenStep input -> Regex input pattern
| WhenStep _ | ThenStep _ -> None
let (|When|_|) (pattern:string) (step) =
match step with
| WhenStep input -> Regex input pattern
| GivenStep _ | ThenStep _ -> None
let (|Then|_|) (pattern:string) (step) =
match step with
| ThenStep input -> Regex input pattern
| GivenStep _ | WhenStep _ -> None
let (|Int|) s = System.Int32.Parse(s)
All the code to TickSpec including the parser and this sample (in Examples/Functional) are available at: http://tickspec.codeplex.com
NUnit
Finally the tests scenarios can be easily run within an NUnit test runner using the TestCaseSource attribute to parameterize a test method:
[<TestFixture>]
type AdditionFixture () =
[<Test>]
[<TestCaseSource(typeof<ScenarioSource>,"Scenarios")>]
member this.TestScenario (scenario:ScenarioSource) =
scenario.Steps |> Seq.fold performStep (Calculator.Create())
|> ignore
member this.Scenarios =
let feature = parse "Addition.txt"
feature.Scenarios
|> Seq.filter (fun scenario ->
scenario.Tags |> Seq.exists ((=) "ignore") |> not
)
Launch the NUnit Test Runner and the test passes:
References