Property-based Testing a Vending Machine

In an earlier article and video, I discussed my implementation of a Vending Machine kata. The code I wrote was dictated via TDD.

I then decided to practice Property-based Testing.

The following property tests were created:

(*Property Tests*)
open FsCheck
open FsCheck.Xunit

[<Property(MaxTest = 10000, QuietOnSuccess = true)>]
let ``balance of coins can never be less than zero`` () =

    Arb.generate<Coin list>
    |> Arb.fromGen
    |> Prop.forAll 
    <| fun coins -> balanceOf coins >= 0m

[<Property(MaxTest = 10000, QuietOnSuccess = true)>]
let ``getChange can never have more than (2) dimes`` () =

    Arb.generate<decimal>
    |> Gen.filter (fun balance -> balance < 200m)
    |> Arb.fromGen
    |> Prop.forAll 
    <| fun balance -> balance |> getChange
                              |> List.filter (fun c -> c = Dime)
                              |> List.length < 3

[<Property(MaxTest = 10000, QuietOnSuccess = true)>]
let ``getChange can never have more than (2) nickels`` () =

    Arb.generate<decimal>
    |> Gen.filter (fun balance -> balance < 200m)
    |> Arb.fromGen
    |> Prop.forAll 
    <| fun balance -> balance |> getChange
                              |> List.filter (fun c -> c = Nickel)
                              |> List.length < 3

These tests actually caught some bugs that surprised me. Specifically, I learned that some functions may be unsafe unless it’s qualified to use and encapsulated within another function.

My refactored domain logic is as follows:

module VendingMachine

(*Types*)
type Coin =    Quarter | Dime | Nickel
               member this.ValueOf() =
                match this with
                    | Quarter -> 0.25m
                    | Dime    -> 0.10m
                    | Nickel  -> 0.05m

type Product = Chips | Soda | Gum
               member this.CostOf() =
                match this with
                | Chips -> 0.25m
                | Soda  -> 0.50m
                | Gum   -> 0.10m 
                
type Purchase = { Product:Product ; Balance:Coin list }

type SelectionResult =
    | Purchased of Purchase
    | Requires  of decimal


(*Functions*)
let balanceOf (coins:Coin list) =
    
    (0.0m , coins) 
    ||> List.fold (fun balance coin -> balance + (coin.ValueOf()))

let getChange balance =
    
    let rec get remaining change =

        if remaining >= Quarter.ValueOf()
        then get (remaining - Quarter.ValueOf()) (Quarter::change)

        elif remaining >= Dime.ValueOf()
        then get (remaining - Dime.ValueOf()) (Dime::change)

        elif remaining >= Nickel.ValueOf()
        then get (remaining - Nickel.ValueOf()) (Nickel::change)

        else change

    get balance []


let select product balance =

    let attemptPurchase (product:Product) deposited =

        let remaining balance (product:Product) =  product.CostOf() - balance

        if balanceOf deposited >= product.CostOf()
        then Purchased { Product=product
                         Balance= getChange (balanceOf deposited - product.CostOf()) }
        else Requires (product |> remaining (balanceOf deposited))

    balance |> attemptPurchase product

Here are my unit tests:

(*Unit Tests*)
open NUnit.Framework
open FsUnit

[<Test>]
let ``depositing quarter results in $.25 balance``() =
    
    [Quarter] |> balanceOf
              |> should equal 0.25

[<Test>]
let ``depositing dime results in $.10 balance``() =
    
    [Dime] |> balanceOf
           |> should equal 0.10

[<Test>]
let ``depositing nickel results in $.05 balance``() =
    
    [Nickel] |> balanceOf
             |> should equal 0.05

[<Test>]
let ``depositing quarter can purchase chips``() =
    
    [Quarter]
    |> select Chips
    |> should equal ((Purchased {Product=Chips ; Balance=[] }))

[<Test>]
let ``depositing two quarters can purchase Soda``() =
    
    [Quarter ; Quarter] 
     |> select Soda
     |> should equal ((Purchased {Product=Soda ; Balance=[] }))

[<Test>]
let ``depositing 2 quarters equals balance of 50 cents`` () =
    [Quarter ; Quarter]
    |> balanceOf
    |> should equal 0.50

[<Test>]
let ``depositing dime can purchase Gum``() =
    
    [Dime]
    |> select Gum
    |> should equal ((Purchased {Product=Gum ; Balance=[] }))

[<Test>]
let ``depositing 1 quarter and attempting to buy soda requires 1 .25 more`` () =
    [Quarter]
    |> select Soda
    |> should equal (Requires 0.25m)

[<Test>]
let ``zero balance and attempting to buy soda requires .50 more`` () =
    []
    |> select Soda
    |> should equal (Requires 0.50m)

[<Test>]
let ``0.75 balance and attempting to buy soda returns 0.25`` () =
    [Quarter ; Quarter ; Quarter]
    |> select Soda
    |> should equal (Purchased {Product=Soda ; Balance=[Quarter] })

[<Test>]
let ``0.85 balance and attempting to buy soda returns 0.35`` () =
    [Quarter ; Quarter ; Quarter ; Dime]
    |> select Soda
    |> should equal (Purchased {Product=Soda ; Balance=[Dime ; Quarter] })
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: