Phillip Trelford's Array

POKE 36879,255

Functional Style BDD Steps

    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.


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#:


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"
|> Seq.iter (fun scenario ->
    |> 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] ->
    | _ -> sprintf "Unmatched line %d" line.Number |> invalidOp


Then the feature test succeeds!



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:

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:



Finally the tests scenarios can be easily run within an NUnit test runner using the TestCaseSource attribute to parameterize a test method:

type AdditionFixture () =
    member this.TestScenario (scenario:ScenarioSource) =
        scenario.Steps |> Seq.fold performStep (Calculator.Create())
        |> ignore  
    member this.Scenarios =       
        let feature = parse "Addition.txt"        
        |> Seq.filter (fun scenario ->
            scenario.Tags |> Seq.exists ((=) "ignore") |> not


Launch the NUnit Test Runner and the test passes:



Comments are closed