Home Twitter Feed

Unit Testing Fable with Mocha

Recently I have been playing around with Fable, an F# to JavaScript compiler. Combined with Elmish, an F# implementation of the Elm Architecture, it has been a joy so far. I find that I spend more time writing code, and less time in the browser. What has surprised me most is that the type system feels much more lightweight than TypeScript, and it gets less in your way.

Trying to parse a custom document format with Fable.Parsimmon I quickly found the need for unit tests, and spent some time figuring out how to get Mocha up and running, based in part on code from Thoth. The following walk-through is based on the minimal F# sample.

1. Make dotnet fable available at the root

We are going to use the dotnet fable command often, and to make it accessible from the root directory, add a build.proj:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
        <DotNetCliToolReference Include="dotnet-fable" Version="2.0.9" />
    </ItemGroup>
</Project>

Install the CLI tool by running dotnet restore.

2. Add tests

Next we will need a separate build project for the tests. Create a test-folder and add Test.fsproj:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
        <ProjectReference Include="../src/App.fsproj" />
    </ItemGroup>
    <ItemGroup>
        <Compile Include="Util.fs" />
        <Compile Include="Main.fs" />
    </ItemGroup>
    <ItemGroup>
        <DotNetCliToolReference Include="dotnet-fable" Version="2.0.9" />
    </ItemGroup>
</Project>

Mocha adds most of it methods as globals, and to access those we need to add some functions for interoperability. Add them to test/Util.fs:

module Test.Util

open Fable.Core
open Fable.Core.Testing

let [<Global>] describe (name: string) (f: unit -> unit) = jsNative
let [<Global>] it (msg: string) (f: unit -> unit) = jsNative

let assertEqual expected actual: unit =
    Assert.AreEqual(actual, expected)

Finally we are ready to add tests. In this case we are ensuring that 1 equals 1. Add test/Main.fs:

module Test.Main

open Test.Util

let run () =
    describe "tests" <| fun () ->
        it "asserts 1 is 1" <| fun () ->
            assertEqual 1 1

run ()

3. Configure Mocha and Webpack

Next we add Mocha as a dependency: npm install -D mocha. To compile the F# tests to JavaScript that Mocha can run, we will need a separate Webpack configuration. Add webpack.test.config.js to the root folder:

var path = require("path");
var webpack = require("webpack");

module.exports = {
    mode: "development",
    entry: "./test/Test.fsproj",
    output: {
        path: path.join(__dirname, "./test/bin"),
        filename: "bundle.js",
    },
    module: {
        rules: [{
            test: /\.fs(x|proj)?$/,
            use: "fable-loader"
        }]
    },
    plugins: [
        new webpack.NamedModulesPlugin()
    ]
}

4. Running the tests

Running the tests is a two-step process:

  1. First you need to compile the F# to JavaScript:

     dotnet fable webpack -- --config webpack.test.config.js
  2. Next, run the tests with Mocha:

     dotnet fable mocha -- test/bin/bundle.js

If everything went well, you should have 1 passing test:

$ dotnet fable mocha -- test/bin/bundle.js

    tests
    ✓ asserts 1 is 1


    1 passing (15ms)

Closing Fable daemon...
$

5. Run tests continuously

In practice you will want to run both Webpack and Mocha in watch mode, so the tests automatically build and run when you change your code.

First, add both commands to the scripts-section of package.json:

"scripts": {
    "test-build": "webpack --config webpack.test.config.js",
    "test-run": "mocha test/bin/bundle.js"
}

Start Webpack in watch mode with dotnet fable yarn-run test-build -- -w. Now the F#-code will automatically be compiled to JavaScript whenever you change the code. In another terminal, start Mocha in watch mode: dotnet fable yarn-run test-run -- -w

That is it. You can see the full diff on Github.

Posted on November 13, 2018