Using NUnit with Fable

Write unit tests compatible with both .NET and JS

This tutorial shows how to compile NUnit tests so they can be run in JS with Mocha. Fable's own tests are written using this method. However, please note this is not a core feature of Fable as it comes through a plugin. You can view the source code, package.json and fableconfig.json on GitHub. This page shows the full source code of the demo.

Configuring Fable and packages

Fable projects usually include a package.json and a fableconfig.json files. Let's have a look at the first one:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
{
  "private": true,
  "dependencies": {
    "fable-core": "^0.1.6",
    "isomorphic-fetch": "^2.2.1"
  },
  "devDependencies": {
    "fable-import-fetch": "^0.0.2",
    "fable-plugins-nunit": "^0.0.3",
    "mocha": "^2.5.3"
  },
  "scripts": {
    "test": "mocha out"
  },
  "engines": {
    "fable": ">=0.3.18"
  }
}

There're several interesting things going on here:

  • First we install our npm dependencies: fable-core which is necessary for all Fable projects and isomorphic-fetch in order to use Fetch API on node (see below).
  • Then we install the development dependencies: If we were to distribute this package, these dependencies wouldn't be installed on the machine of the final consumers as they are only necessary for development. fable-import-fetch is the F# type definition for Fetch API, fable-plugins-nunit will extend Fable's capabilities to allow NUnit tests compilation and mocha is our test runner on JS.
  • We define a npm script: So tests will be run when calling npm test (or npm run-script test). The script is as simple as calling mocha and passing the directory where the tests compiled to JS are to be found.
  • Finally we force the user to compile the project using fable-compiler@0.3.18 or higher.

Let's check now fableconfig.json:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
{
    "module": "commonjs",
    "projFile": "index.fsx",
    "outDir": "out",
    "plugins": "node_modules/fable-plugins-nunit/Fable.Plugins.NUnit.dll",
    "scripts": {
        "prebuild": "npm install",
        "postbuild": "npm test"
    }
}

As in other samples, we specify the JS module system to target, the F# projFile and the outDir where to put the compiled JS files. But the interesting part here is we pass Fable.Plugins.NUnit.dll through the plugins parameter (which can also be an array) and with scripts.postbuild we make Fable run npm test (defined in package.json above) after building the project.

Referencing Fable and dependencies

As this is a simple F# script, we make a reference to Fable.Core.dll with the #r directive and open the appropriate namespaces.

1: 
2: 
3: 
4: 
5: 
6: 
#r "System.Threading.dll"
#r "node_modules/fable-core/Fable.Core.dll"

open System
open Fable.Core
open Fable.Import

The demo uses the Fetch API and, as we'll be running the tests on node, we have to polyfill it using the isomorphic-fetch package.

1: 
2: 
3: 
4: 
5: 
#load "node_modules/fable-import-fetch/Fable.Import.Fetch.fs"

open Fable.Import.Fetch

JsInterop.importAll "isomorphic-fetch"

Here we're using the testing methods and attributes from Fable.Core. If you're referencing NUnit library instead, just comment out the next line and adapt the two lines below to the path where NUnit can be found on your machine.

1: 
2: 
3: 
4: 
open Fable.Core.Testing

// #r "../../../packages/NUnit/lib/nunit.framework.dll"
// open NUnit.Framework

Now we can write tests as we would do with NUnit. The most commonly used attributes (TestFixture and Test) and their respective SetUp/TearDown counterparts are implemented. For assertions, however, only Assert.AreEqual is available. But more features will be available soon.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
[<TestFixture>]
module MyTests =

  // Convenience method
  let equal (expected: 'T) (actual: 'T) =
      Assert.AreEqual(expected, actual)

  [<Test>]
  let ``Structural comparison with arrays works``() =
    let xs1 = [| 1; 2; 3 |]
    let xs2 = [| 1; 2; 3 |]
    let xs3 = [| 1; 2; 4 |]
    equal true (xs1 = xs2)
    equal false (xs1 = xs3)
    equal true (xs1 <> xs3)
    equal false (xs1 <> xs2)
  
  [<Test>]
  let ``Set.intersectMany works``() =
      let xs = set [1; 2]
      let ys = Set.singleton 2
      let zs = set [2; 3]
      let ks = Set.intersectMany [xs; ys; zs] 
      (ks.Contains 2 && not(ks.Contains 1 || ks.Contains 3))
      |> equal true

With some limitations, it's also possible to write asynchronous tests. For this, you just need to wrap the whole test with async { ... } |> Async.RunSynchronously.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
  [<Test>]
  let ``Async.Parallel works``() =
    async {
        let getWebPageLength url =
            async {
                let! res = GlobalFetch.fetch(Url url) |> Async.AwaitPromise
                let! txt = res.text() |> Async.AwaitPromise 
                return txt.Length
            }
        let! results =
          [ "http://fable-compiler.github.io"
            "http://babeljs.io"
            "http://fsharp.org" ]
          |> List.map getWebPageLength
          |> Async.Parallel
        // The sum of lenghts of all web pages is
        // expected to be bigger than 100 characters
        (Array.sum results) > 100 |> equal true
    } |> Async.RunSynchronously

Note: Besides the tests, Async.RunSynchronously is not compatible with Fable as asynchronous operations are not allowed to lock the main thread in JS.

namespace System
namespace Fable
namespace Fable.Core
namespace Fable.Import
module Fetch

from Fable.Import
module JsInterop

from Fable.Core
val importAll : path:string -> 'T

Full name: Fable.Core.JsInterop.importAll
module Testing

from Fable.Core
Multiple items
type TestFixtureAttribute =
  inherit Attribute
  new : unit -> TestFixtureAttribute

Full name: Fable.Core.Testing.TestFixtureAttribute

--------------------
new : unit -> TestFixtureAttribute
module MyTests

from Index
val equal : expected:'T -> actual:'T -> unit

Full name: Index.MyTests.equal
val expected : 'T
val actual : 'T
type Assert =
  static member AreEqual : x:'T * y:'T -> unit

Full name: Fable.Core.Testing.Assert
static member Assert.AreEqual : x:'T * y:'T -> unit
Multiple items
type TestAttribute =
  inherit Attribute
  new : unit -> TestAttribute

Full name: Fable.Core.Testing.TestAttribute

--------------------
new : unit -> TestAttribute
val ( Structural comparison with arrays works ) : unit -> unit

Full name: Index.MyTests.( Structural comparison with arrays works )
val xs1 : int []
val xs2 : int []
val xs3 : int []
val ( Set.intersectMany works ) : unit -> unit

Full name: Index.MyTests.( Set.intersectMany works )
val xs : Set<int>
val set : elements:seq<'T> -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.set
val ys : Set<int>
Multiple items
module Set

from Microsoft.FSharp.Collections

--------------------
type Set<'T (requires comparison)> =
  interface IComparable
  interface IEnumerable
  interface IEnumerable<'T>
  interface ICollection<'T>
  new : elements:seq<'T> -> Set<'T>
  member Add : value:'T -> Set<'T>
  member Contains : value:'T -> bool
  override Equals : obj -> bool
  member IsProperSubsetOf : otherSet:Set<'T> -> bool
  member IsProperSupersetOf : otherSet:Set<'T> -> bool
  ...

Full name: Microsoft.FSharp.Collections.Set<_>

--------------------
new : elements:seq<'T> -> Set<'T>
val singleton : value:'T -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.singleton
val zs : Set<int>
val ks : Set<int>
val intersectMany : sets:seq<Set<'T>> -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.intersectMany
member Set.Contains : value:'T -> bool
val not : value:bool -> bool

Full name: Microsoft.FSharp.Core.Operators.not
val ( Async.Parallel works ) : unit -> unit

Full name: Index.MyTests.( Async.Parallel works )
val async : AsyncBuilder

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
val getWebPageLength : (string -> Async<int>)
val url : string
val res : Response
type GlobalFetch =
  static member fetch : req:RequestInfo * ?init:RequestInit -> Promise<Response>

Full name: Fable.Import.Fetch.GlobalFetch
static member GlobalFetch.fetch : req:RequestInfo * ?init:RequestInit -> JS.Promise<Response>
union case RequestInfo.Url: string -> RequestInfo
Multiple items
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken

Full name: Microsoft.FSharp.Control.Async

--------------------
type Async<'T>

Full name: Microsoft.FSharp.Control.Async<_>
static member Async.AwaitPromise : promise:JS.Promise<'T> -> Async<'T>
val txt : string
abstract member Body.text : unit -> JS.Promise<string>
property String.Length: int
val results : int []
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member GetSlice : startIndex:int option * endIndex:int option -> 'T list
  member Head : 'T
  member IsEmpty : bool
  member Item : index:int -> 'T with get
  member Length : int
  member Tail : 'T list
  static member Cons : head:'T * tail:'T list -> 'T list
  static member Empty : 'T list

Full name: Microsoft.FSharp.Collections.List<_>
val map : mapping:('T -> 'U) -> list:'T list -> 'U list

Full name: Microsoft.FSharp.Collections.List.map
static member Async.Parallel : computations:seq<Async<'T>> -> Async<'T []>
type Array =
  member Clone : unit -> obj
  member CopyTo : array:Array * index:int -> unit + 1 overload
  member GetEnumerator : unit -> IEnumerator
  member GetLength : dimension:int -> int
  member GetLongLength : dimension:int -> int64
  member GetLowerBound : dimension:int -> int
  member GetUpperBound : dimension:int -> int
  member GetValue : [<ParamArray>] indices:int[] -> obj + 7 overloads
  member Initialize : unit -> unit
  member IsFixedSize : bool
  ...

Full name: System.Array
val sum : array:'T [] -> 'T (requires member ( + ) and member get_Zero)

Full name: Microsoft.FSharp.Collections.Array.sum
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:Threading.CancellationToken -> 'T
Fork me on GitHub