Home > Uncategorized > FsCheck–Unit testing in F#

FsCheck–Unit testing in F#

Ayende posted another hiring question; here and here. Ignoring the fact that I think that this is too big a task for such an occasion I sat down and tried to come up with a solution. To keep things short and clear I picked F# as the language of my choice. Hold on; here comes the phone book library, all in one piece:

module PhoneBookLibrary =

    open System

    open System.Collections.Generic

    open System.IO

    open System.Runtime.Serialization.Formatters.Binary

 

    type PhoneType = Work | Cellphone | Home

 

    type Entry = { mutable id : option<Guid>; firstName : string; lastName : string; phone : option<PhoneType>; number : option<string> }

 

    type EnumerationMethod = FirstName | LastName

 

    type Index<‘a>() =

        let map = new SortedDictionary<‘a,List<int>>()

 

        member this.Lookup(value : ‘a) : List<int> =

            map.[value]

 

        member this.Add(value : ‘a, i : int) : unit =

            if map.ContainsKey(value) |> not then

                map.[value] <- new List<int>()

            map.[value].Add(i)

 

        member this.Remove(value : ‘a, i: int) : unit =

            map.[value].Remove(i) |> ignore

 

        member this.Values

            with get() : seq<int> =                        

                seq {

                    for v in map.Values do

                        for i in v do

                            yield i

                }

 

    type PhoneBook() =       

        let entries = new List<Entry>()

        let indexByFirstName = new Index<string>()

        let indexByLastName = new Index<string>()

 

        static member Load(fileName : string) : PhoneBook =

            let formatter = new BinaryFormatter()

            use stream = new FileStream(fileName, FileMode.Open)

            formatter.Deserialize(stream) :?> PhoneBook

 

        member this.Save(fileName : string) : unit =

            let formatter = new BinaryFormatter()

            use stream = new FileStream(fileName, FileMode.Create)

            formatter.Serialize(stream, this)

 

        member this.FindByFirstName(firstName : string) : seq<Entry> =

            indexByFirstName.Lookup(firstName) |> Seq.map (fun i -> entries.[i])       

 

        member this.FindByLastName(lastName : string) : seq<Entry> =

            indexByLastName.Lookup(lastName) |> Seq.map (fun i -> entries.[i])       

 

        member this.Create (e : Entry) : unit =        

            assert(Option.isNone e.id)

            e.id <- Some(Guid.NewGuid())

            let i = entries.Count

            entries.Add(e)

            indexByFirstName.Add(e.firstName, i)

            indexByLastName.Add(e.lastName, i)

 

        member this.Update (e’ : Entry) : unit =

            assert(Option.isSome e’.id)

            let i = this.IndexOf(e’)

            let e = entries.[i]

            entries.[i] <- e’

            if e’.firstName <> e.firstName then

                indexByFirstName.Remove(e.firstName, i)

                indexByFirstName.Add(e’.firstName, i)           

            if e’.lastName <> e.lastName then

                indexByLastName.Remove(e.lastName, i)

                indexByLastName.Add(e’.lastName, i)

 

        member this.Delete (e : Entry) : unit =

            assert(Option.isSome e.id)

            let i = this.IndexOf(e)

            let ilast = entries.Count-1

            let elast = entries.[ilast]

            entries.[i] <- elast

            entries.RemoveAt(ilast);

            indexByFirstName.Remove(e.firstName, i)             

            indexByLastName.Remove(e.lastName, i)

            if i <> ilast then

                indexByFirstName.Remove(elast.firstName, ilast)             

                indexByLastName.Remove(elast.lastName, ilast)

                indexByFirstName.Add(elast.firstName, i)             

                indexByLastName.Add(elast.lastName, i)

 

        member this.Enumerate (em : EnumerationMethod) : seq<Entry> =

            match em with

            | FirstName -> indexByFirstName.Values |> Seq.map (fun i -> entries.[i])

            | LastName -> indexByLastName.Values |> Seq.map (fun i -> entries.[i])

 

        member private this.IndexOf(template : Entry) =       

            assert(Option.isSome template.id)

            entries.FindIndex(new Predicate<Entry>(fun x -> x.id = template.id))       

 

Oups. Sorry for that. Anyway, I needed to test my code and I hade a look at FsCheck and I was really impressed. Look at this:

module PhoneBookLibraryTest =

    open PhoneBookLibrary

    open FsCheck

 

    let generateIndex<‘a> =

        gen {

            let index = new Index<‘a>()

            let! count = Gen.choose(0, 10)

            for i in 1..count do

                let! v, i = Arb.generate<‘a*int>

                index.Add(v,i)

            return index

        }

 

    let generateEntry =

        gen {

            let! fn = Gen.elements [ “Daniel”; “Anders”; “Roy”; “Bertil” ]

            let! ln = Gen.elements [ “Svensson”; “Andersson”; “Fredriksson”; “Abrahamsson”; “Patriksson” ];

            let! t = Gen.elements [ None; Some(Work); Some(Cellphone); Some(Home)];

            let! no = Gen.elements [  None; Some(“12346”); Some(“12347”); Some(“12348”); Some(“12349”)];

            return { id = Option.None; firstName = fn; lastName = ln; phone = t; number = no }

        }

 

    let generatePhoneBook =

        gen {

            let! numEntries = Gen.choose (0, 100)

            let result = new PhoneBook()

            for i in 1..numEntries do

                let! e = generateEntry

                result.Create(e)

            return result

        }

 

    let generateEnumerationMethod = Gen.elements [ EnumerationMethod.FirstName; EnumerationMethod.LastName ]

 

    type CustomGenerators =

        static member PhoneBook() = Arb.fromGen generatePhoneBook   

        static member EnumerationMethod() = Arb.fromGen generateEnumerationMethod

        static member Entry() = Arb.fromGen generateEntry

        static member Index() = Arb.fromGen generateIndex

 

    Arb.register<CustomGenerators>() |> ignore

 

    // Index.Add, Index.Lookup

    let valuesInIndexCanBeFound (values : list<string*int>) =

        let index = new Index<string>()

        for v, i in values do

            index.Add(v, i)

        values |>

        Seq.forall (fun (v,i) -> index.Lookup(v).IndexOf(i) >= 0)

 

    // Index.Remove

    let valuesCanBeRemovedFromIndex (values : list<string*int*bool>) =

        let index = new Index<string>()

        for v, i, _ in values do

            index.Add(v, i)

        for v, i, deleteFlag in values do

            if deleteFlag then

                index.Remove(v, i)

        let existingValues = index.Values |> Seq.toList |> List.sort

        let expectedValues = values |> List.filter (fun (_,_,x) -> not(x)) |> List.map (fun (_,i,_) -> i) |> List.sort

        assert(existingValues = expectedValues)

        existingValues = expectedValues

 

    // Index.Values

    let indexValuesAreSorted (index : Index<string>) = true // todo

 

    // PhoneBook.FindByFirstName, PhoneBook.FindByLastName

    let everyEntryCanBeBeFoundByFirstNameOrLastName (phoneBook : PhoneBook) (enumerationMethod : EnumerationMethod) (useFirstName : bool) =

        let exists e =

            if useFirstName then

                phoneBook.FindByFirstName(e.firstName)

            else

                phoneBook.FindByLastName(e.lastName)

            |>

            Seq.exists (fun x -> x = e)

        phoneBook.Enumerate(enumerationMethod) |>

        Seq.forall exists

 

    // PhoneBook.Enumerate

    let enumerationOfAPhoneBookIsOrdered (phoneBook : PhoneBook) (enumerationMethod : EnumerationMethod) =

        let entries = phoneBook.Enumerate(enumerationMethod)

        let compare =

            match enumerationMethod with

            | EnumerationMethod.FirstName -> (fun (e1,e2) -> e1.firstName <= e2.firstName)

            | EnumerationMethod.LastName ->(fun (e1,e2) -> e1.lastName <= e2.lastName)

        entries |>

        Seq.pairwise |>   

        Seq.forall compare

 

    // PhoneBook.Save, PhoneBook.Load

    let aSavedPhoneBookShouldLoadToAnEquivalentObject (phoneBook : PhoneBook) =

        let fileName = “c:\\temp\\phonebook.bin”

        phoneBook.Save(fileName)

        let phoneBook’ = PhoneBook.Load(fileName)

        Seq.zip (phoneBook.Enumerate EnumerationMethod.FirstName) (phoneBook’.Enumerate EnumerationMethod.FirstName) |>

        Seq.forall (fun (e1,e2) -> e1 = e2)

 

    // PhoneBook.Create, PhoneBook.Delete

    let entriesCanBeDeleted (entries : list<bool*Entry>) =

        let phoneBook = new PhoneBook()

        for _, e in entries do

            phoneBook.Create(e)

        for deleteFlag, e in entries do

            if deleteFlag then

                phoneBook.Delete(e)

        let numExistingEntries = Seq.length (phoneBook.Enumerate EnumerationMethod.FirstName)

        let numNonDeletedEntries = entries |> Seq.filter (fst>>not) |> Seq.length

        numExistingEntries = numNonDeletedEntries

 

    // PhoneBook.Create, PhoneBook.Update

    let entriesCanBeUpdated (entries : list<Entry*Entry>) =

        List.iter (fun (e,_) -> e.id <- Option.None) entries

        let phoneBook = new PhoneBook()

        for e, e’ in entries do

            phoneBook.Create(e)

            e’.id <- e.id

            phoneBook.Update(e’)

        let updatedEntries = phoneBook.Enumerate EnumerationMethod.FirstName |> Seq.toList

        let targetEntries = entries |> List.map snd |> List.sortBy (fun x -> x.firstName)

        Seq.zip updatedEntries targetEntries |>

        Seq.forall (fun (e1, e2) -> e1 = e2)

 

    let runTests =

        // Index

        Check.Quick valuesInIndexCanBeFound

        Check.Quick valuesCanBeRemovedFromIndex

        Check.Quick indexValuesAreSorted

        // Phonebook

        Check.Quick everyEntryCanBeBeFoundByFirstNameOrLastName

        Check.Quick enumerationOfAPhoneBookIsOrdered

        Check.Quick aSavedPhoneBookShouldLoadToAnEquivalentObject

        Check.Quick entriesCanBeDeleted

        Check.Quick entriesCanBeUpdated

 

 

PhoneBookLibraryTest.runTests

 

Go try it out! The library is also available as a nuget package.

Advertisements
Categories: Uncategorized
  1. No comments yet.
  1. October 29, 2013 at 10:28 pm

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: