Introduction to Property Testing using FsCheck

Jonathan Taylor

Scala Dev @ Cake Solutions*

Agenda

  • History
  • What is Property Testing ?
  • Custom generators 
  • Real World Haskell's qsort example
  • ScalaCheck book's run length encoding example
  • ScalaCheck book's Expression example
  • Gen<'a>

History

  • Haskell's QuickCheck (1999)
  • - Koen Claessen & John Hughes
  • Scala: ScalaCheck
  • JavaScript: JSCheck, JSVerify, TestCheck.js
  • etc.

What is property testing ? (1)

[<Theory>]
[<InlineData(1, 1)>]
[<InlineData(21, 21)>]
[<InlineData(3, 4)>]
[<InlineData(4, 3)>]
[<InlineData(10, 0)>]
[<InlineData(0, 10)>]
let additionCommutative (a: int) (b: int) =
    Assert.Equal(a + b, b + a)
[<Fact>]
let ``prop_additionCommutative``() =
  FsCheck.Check.QuickThrowOnFailure <| fun (a: int) (b: int) ->
    a + b = b + a

Random test case generation

[<Property>]
let ``prop_additionCommutative2``(a: int) (b: int) =
  a + b = b + a

What is property testing ? (2)

let rec qsort = function
  | x :: xs ->
    let lhs, rhs = List.partition (fun x' -> x' < x) xs
    qsort lhs @ List.singleton x @ qsort rhs
  | _ ->
    []
[<Property>]
let ``prop_idempotent`` (xs: int list) =
  qsort (qsort xs) = qsort xs
  
[<Property>]
let ``prop_minimum`` (xs: int list) =
  not (List.isEmpty xs) ==>
    lazy (List.head (qsort xs) = minimum xs)

Thinking in terms of properties/laws

  • A property of a program is an observation that we expect to hold true regardless of the program’s inputs
  • The tool finds corner-cases where the specification is violated, which leads to either the code or the specification getting fixed

Gen<'a>

  • constant
  • choose
  • elements
  • oneof
  • frequency
  • listOf
  • listOfLength
  • shuffle
  • where

constant

printfn "%A" (sample 5 5 (constant 42))

// [42; 42; 42; 42; 42]

choose

printfn "%A" (sample 5 5 (choose (1, 20)))

// [3; 4; 17; 18; 11]

elements

printfn "%A" (sample 5 5 (elements [1..10]))

// [1; 8; 3; 5; 4]

oneof

printfn "%A" (sample 5 5 (oneof [
                                elements [1..10]
                                elements [11..20]
                                elements [21..30]
                            ]))

// [14; 22; 26; 3; 16]

frequency

printfn "%A" (sample 5 10 (frequency [
                                20, elements [1..10]
                                30, elements [11..20]
                                50, elements [21..30]
                            ]))

// [10; 21; 19; 10; 30; 30; 21; 30; 27; 22]

listOf

printfn "listOf: %A" (sample 5 5 (choose (1, 10) |> Gen.listOf))

// listOf: [[5]; []; [9; 4; 5]; [6; 9]; [3]]

listOfLength

printfn "listOfLength: %A" (sample 5 5 (choose (1, 10) |> Gen.listOfLength 5))

// listOfLength: [[2; 8; 8; 1; 6]; [4; 2; 8; 8; 1]; [10; 4; 2; 8; 8]; [10; 10; 4; 2; 8];
// [4; 10; 10; 4; 2]]

shuffle

printfn "shuffle: %A" (sample 5 5 (shuffle [1;2;3;4;5]))

// shuffle: [[|1; 3; 2; 5; 4|]; [|4; 1; 3; 2; 5|]; [|4; 5; 2; 3; 1|]; [|2; 1; 5; 4; 3|];
// [|2; 3; 4; 5; 1|]]

where

printfn "%A" (sample 5 5 (genInt |> where (fun n -> n > 10)))


// [12; 15; 11; 13; 19]

Arbitrary<'a>

type Arbitrary<'a>() =
    abstract Generator      : Gen<'a>
    abstract Shrinker       : 'a -> seq<'a>

Arb

  • Arb.from<'a>
  • Arb.generate<'a>
  • Arb.fromGen
  • Arb.fromGenShrink
  • Arb.shrink
  • Arb.register

Arb.Default

  • Arb.Default.String
  • Arb.Default.NonEmptyString
  • Arb.Default.Int
  • Arb.Default.NonNegativeInt
  • Arb.Default.Float
  • Arb.Default.NormalFloat
  • etc.

Shrinking

  1. Shrink a term to any of its immediate subterms
  2. Recursively apply ‘shrink’ to all immediate subterms
  3. Type-specific shrinkings such as replacing a constructor by a simpler constructor

Simplifying a counterexample

Functors and applicatives and monads!

Oh my!

Functor

let s1 = Seq.singleton 42 |> Seq.map string
printfn "%A" s1

let l1 = List.singleton 42 |> List.map string
printfn "%A" l1

let a1 = Array.singleton 42 |> Array.map string
printfn "%A" a1

let g1 = Gen.constant 42 |> Gen.map string
printfn "%A" <| sample 0 1 g1

Applicative

let fn n s1 s2 = String.length (sprintf "%d%s%s" n s1 s2) > 5
let gn = genInt
let gs1 = genInt |> map string
let gs2 = genInt |> map (fun n -> (string)(n * 10))

let g2 = map3 fn gn gs1 gs2
printfn "%A" <| sample 5 10 g2

let g3 = gen {
  let! n = gn
  let! s1 = gs1
  let! s2 = gs2
  return fn n s1 s2
}
printfn "%A" <| sample 5 10 g3

// Gen<(a -> b -> c -> d)> <*> Gen<a>
// Gen<(b -> c -> d)> <*> Gen<b>
// Gen<(c -> d)> <*> Gen<c>
// Gen<d>

let g4 = constant fn <*> gn <*> gs1 <*> gs2
printfn "%A" <| sample 5 10 g4

let g5 = map fn gn <*> gs1 <*> gs2
printfn "%A" <| sample 5 10 g5

let g6 = fn <!> gn <*> gs1 <*> gs2
printfn "%A" <| sample 5 10 g6

Monad

// Trying to implement Gen.oneof using 'map'.
let oneofv1 (gens: Gen<_> seq) =
  choose(0, Seq.length gens - 1)
  |> map (fun index -> Seq.item index gens)

let g7 = oneofv1 [constant 1; constant 2; constant 3]
printfn "g7: %A" <| sample 2 10 (g7 >>= id)

// Implementing Gen.oneof using '>>='.
let oneofv2 (gens: Gen<_> seq) =
  choose(0, Seq.length gens - 1)
  >>= (fun index -> Seq.item index gens)

let g8 = oneofv2 [constant 1; constant 2; constant 3]
printfn "g8: %A" <| sample 2 10 g8

// Implementing Gen.oneof using a 'gen' computation expression.
let oneofv3 (gens: Gen<_> seq) = gen {
  let! index = choose(0, Seq.length gens - 1)
  return! Seq.item index gens
}

let g9 = oneofv3 [constant 1; constant 2; constant 3]
printfn "g9: %A" <| sample 2 10 g9

Links

Made with Slides.com