λ>_

FsCheck setup in F#

08 February 2015

In the previous post I used Cabal to bootstrap QuickCheck with Hspec. In Haskell, Cabal is a build system which also doubles as a package manager.

This post attempts to do the same in F#; bootstrap FsCheck with xUnit.net, but instead of MSBuild and XML, I’m going to use Paket, Fake, and F#.

Install Paket, if not already installed:

// samples-fscheck.fsx

open System
open System.IO

Environment.CurrentDirectory <- __SOURCE_DIRECTORY__

if not (File.Exists "paket.exe") then
    let url = "https://github.com/fsprojects/Paket/releases/download/0.26.3/paket.exe"
    use wc = new Net.WebClient()
    let tmp = Path.GetTempFileName()
    wc.DownloadFile(url, tmp)
    File.Move(tmp, Path.GetFileName url)

Reference Paket and specify the required packages:

// samples-fscheck.fsx

#r "paket.exe"

Paket.Dependencies.Install """
    source https://nuget.org/api/v2
    nuget FsCheck.Xunit
    nuget FAKE
    nuget xunit.runners
""";;

Reference Fake and specify the build parameters:

// samples-fscheck.fsx

#r "packages/FAKE/tools/FakeLib.dll"

open Fake
open Fake.FscHelper

let outputPath = Path.Combine(Environment.CurrentDirectory, "bin")
let outputFile = @"bin\GettingStarted.dll";
let references =
    [ "packages/xunit/lib/net20/xunit.dll";
      "packages/FsCheck/lib/net45/FsCheck.dll";
      "packages/FsCheck.Xunit/lib/net45/FsCheck.Xunit.dll" ]

Use Fake’s DSL to define the build tasks:

// samples-fscheck.fsx

Target "CreateOutputPath" (fun _ -> 
    CreateDir outputPath)

Target "CleanOutputPath" (fun _ ->
    CleanDir outputPath)

Target "CompileFiles" (fun _ ->
    [ "tests/GettingStarted.fs" ]
    |> Fsc (fun options ->
        { options with
            Output = outputFile
            FscTarget = Library
            References = references }))

Target "CopyAssemblyReferences" (fun _ ->
    CopyFiles outputPath references)

Target "RunTests" (fun _ ->
    !! outputFile
    |> xUnit (fun options -> options))

Compose the build tasks into a build pipeline:

// samples-fscheck.fsx

"CreateOutputPath"
    ==> "CleanOutputPath"
    ==> "CompileFiles"
    ==> "CopyAssemblyReferences"
    ==> "RunTests"

RunTargetOrDefault "RunTests"

Icebreaker:

Add a simple (icebreaker) test, because there’s always a bit of work involved in getting everything up and running:

// tests/GettingStarted.fs

module GettingStarted

open System
open FsCheck.Xunit

let add x y = x + y

[<Property>]
let ``icebreaker`` x y =
    add x y = add y x

Run a build:

Run the following command in a terminal:

fsi samples-fscheck.fsx

See the tests running, and passing, via xUnit.net’s built-in test runner:

fsi samples-fscheck

found: <Path>\samples-fscheck\paket.dependencies
Resolving packages:
    - exploring FAKE 3.14.9
    - exploring xunit 1.9.2
    - exploring xunit.runners 1.9.2
    - exploring FsCheck 1.0.4
    - exploring FsCheck.Xunit 1.0.4
Locked version resolutions written to <Path>\samples-fscheck
\paket.lock
Building project with version: LocalBuild
Shortened DependencyGraph for Target RunTests:
<== RunTests
<== CopyAssemblyReferences
<== CompileFiles
<== CleanOutputPath
<== CreateOutputPath

The resulting target order is:
 - CreateOutputPath
 - CleanOutputPath
 - CompileFiles
 - CopyAssemblyReferences
 - RunTests
Starting Target: CreateOutputPath
<Path>\samples-fscheck\bin already exists.
Finished Target: CreateOutputPath
Starting Target: CleanOutputPath (==> CreateOutputPath)
Deleting contents of <Path>\samples-fscheck\bin
Finished Target: CleanOutputPath
Starting Target: CompileFiles (==> CleanOutputPath)
FSC with args:[|"-o"; "bin\GettingStarted.dll"; "-a"; "--platform:anycpu";
  "--reference:packages/xunit/lib/net20/xunit.dll";
  "--reference:packages/FsCheck/lib/net45/FsCheck.dll";
  "--reference:packages/FsCheck.Xunit/lib/net45/FsCheck.Xunit.dll";
  "tests/GettingStarted.fs"|]
Finished Target: CompileFiles
Starting Target: CopyAssemblyReferences (==> CompileFiles)
Finished Target: CopyAssemblyReferences
Starting Target: RunTests (==> CopyAssemblyReferences)
<Path>\samples-fscheck\packages\xunit.runners\tools\xunit.co
nsole.clr4.exe "<Path>\samples-fscheck\bin\GettingStarted.dl
l"
xUnit.net console test runner (64-bit .NET 4.0.30319.34209)
Copyright (C) 2013 Outercurve Foundation.

xunit.dll:     Version 1.9.2.1705
Test assembly: <Path>\samples-fscheck\bin\GettingStarted.dll


1 total, 0 failed, 0 skipped, took 0.558 seconds
Finished Target: RunTests

---------------------------------------------------------------------
Build Time Report
---------------------------------------------------------------------
Target                   Duration
------                   --------
CreateOutputPath         00:00:00.0018707
CleanOutputPath          00:00:00.0046358
CompileFiles             00:00:03.4156059
CopyAssemblyReferences   00:00:00.0058707
RunTests                 00:00:01.9816487
Total:                   00:00:05.4737549
Status:                  Ok
---------------------------------------------------------------------

See the test failing, as a form of Double Entry Bookkeeping:

// Change:
add x y = add y x

// To:
add x y = add 0 1

Re-run the command in a terminal:

fsi samples-fscheck

xUnit.net console test runner (64-bit .NET 4.0.30319.34209)
Copyright (C) 2013 Outercurve Foundation.

xunit.dll:     Version 1.9.2.1705
Test assembly: <Path>\samples-fscheck\bin\GettingStarted.dll


GettingStarted.icebreaker [FAIL]

   Falsifiable, after 1 test (2 shrinks) (StdGen (1751373120,295969456)):
   (0, 0)
   Stack Trace:
   sorry no stacktrace

1 total, 1 failed, 0 skipped, took 0.679 seconds
Running build failed.

For convenience, all the above are also available on GitHub.

References