F#: Unit Tests without Declaring Stages

Datetime:2016-08-22 22:02:51          Topic: F#  Unit Testing           Share

Intro

Unit Tests in F# don’t require a developer to declare the stages of the test within the test code. What does that really mean?

The old way I use to write a unit test

Take the following example:

[<Test>]
let ``Vending machine denies selection with insufficient funds``() =
   // Setup
   let balance = Quarter |> insert []

   // Test
   let display = balance |> select Pepsi

   // Verify
   display |> should equal (Denied {Deposited=0.25; Requires=1.00})

Observe how the test is partitioned into stages (i.e. setup, test, and verify). I learned to document the stages of my unit tests from the book XUnit Test Patterns . However, even though this test screams intent (based on test stages), it is also somewhat verbose and could be refactored.

The new way I write a unit test

Let’s refactor the old test where the stages are no longer explicit but instead are apparent via the control flow of operations from setup to verification.

The following code reflects the refactored result of the old test:

[<Test>]
let ``Vending machine denies selection with insufficient funds``() =
   Quarter |> insert []
           |> select Pepsi
           |> ``isDenied?``
           |> should equal true

Observe how the above code just reads like a story. Hence, it’s not fragmented with reminders of “this is the setup stage” or “this is the verification stage”. No. The F# syntax encourages readability without blatant reminders.

Test Suite

Here’s some other tests that I wrote:

module Tests

open FsUnit
open NUnit.Framework
open Machine

[<Test>]
let ``Vending machine that's out of service reflects out of service``() =
   display OutOfService 
   |> should equal "Out of Service"

[<Test>]
let ``Vending machine that's waiting for selection service reflects waiting for selection``() =
   display WaitingForSelection 
   |> should equal "Make selection"

[<Test>]
let ``Vending machine receives payment and reflects balance``() =
   display (PaymentReceived OneDollarBill) 
   |> should equal "$1.00"

[<Test>]
let ``Vending machine receives partial payment for selection and reflects required amount``() =
   display (NotPaidInFull { Requires = 1.00; Deposited=0.25 }) 
   |> should equal "Requires $0.75 more"

[<Test>]
let ``get balance``() =
   [Nickel; Dime; Quarter] |> getBalance 
   |> should equal 0.40

[<Test>]
let ``Vending machine accepts quarter``() =
   Quarter |> insert [] 
           |> should equal 0.25

[<Test>]
let ``Vending machine accepts dollar``() =
   OneDollarBill |> insert [] 
                 |> should equal 1.00

[<Test>]
let ``get price of product``() =
   getPrice Pepsi |> should equal 1.00

[<Test>]
let ``Vending machine denies selection with insufficient funds``() =
   Quarter |> insert [] 
           |> select Pepsi 
           |> ``isDenied?``
           |> should equal true

[<Test>]
let ``Vending machine grants selected product``() =
   OneDollarBill |> insert [] 
                 |> select Pepsi 
                 |> should equal (Granted Pepsi)

Domain

Here’s the domain that the tests target:

module Machine

type Deposit =
    | Nickel
    | Dime
    | Quarter
    | OneDollarBill
    | FiveDollarBill

type TransactionAttempt = { 
    Deposited:float
    Requires:float 
}

type State =
    | OutOfService
    | PaymentReceived of Deposit
    | WaitingForSelection
    | NotPaidInFull of TransactionAttempt

type Product =
    | Pepsi
    | Coke
    | Sprite
    | MountainDew

type RequestResult =
    | Denied of TransactionAttempt
    | Granted of Product

(* Functions *)
open System

let display = function
    | OutOfService            -> "Out of Service"
    | WaitingForSelection     -> "Make selection"
    | NotPaidInFull attempt   -> sprintf "Requires %s more" ((attempt.Requires - attempt.Deposited).ToString("C2"))
    | PaymentReceived deposit -> match deposit with
                                 | Nickel         -> "5¢"
                                 | Dime           -> "10¢"
                                 | Quarter        -> "25¢"
                                 | OneDollarBill  -> "$1.00"
                                 | FiveDollarBill -> "$5.00"

let getBalance coins =
    coins |> List.fold (fun acc d -> match d with
                                     | Nickel         -> acc + 0.05
                                     | Dime           -> acc + 0.10
                                     | Quarter        -> acc + 0.25
                                     | OneDollarBill  -> acc + 1.00
                                     | FiveDollarBill -> acc + 5.00) 0.00

let insert balance coin =
    coin::balance |> getBalance

let getPrice = function
    | Pepsi       -> 1.00
    | Coke        -> 1.00
    | Sprite      -> 1.00
    | MountainDew -> 1.00

let select product balance =
    let attempt = { Deposited=balance
                    Requires=product |> getPrice }

    let paidInFull = attempt.Deposited >= attempt.Requires

    if not paidInFull then 
        Denied attempt
    else Granted product

let ``isDenied?`` = function
    | Granted _ -> false | Denied _ -> true

Conclusion

In conclusion, unit Tests in F# don’t require sections. Hence, the control flow that F# naturally provides removes the need to scream what stage of a test a particular piece of code belongs to. In addition, F#’s syntax enables a sentence like control flow that is self documenting without having to comment the stages of a test.





About List