Fable library for testing. Inspired by the popular Expecto library for F# and adopts the testList
, testCase
and testCaseAsync
primitives for defining tests.
The tests themselves are written once and can run:
- Inside node.js using Mocha
- Inside the browser (standalone)
- Inside a headless browser
- Inside dotnet with Expecto
- To Do: Inside dotnet standalone (PR's are welcome)
Install the Fable binding from Nuget
# using nuget
dotnet add package Fable.Mocha
Assuming you have you the following project structure:
repo
|
|-- package.json
|-- src
| -- YourLibrary.fsproj
| -- Library.fs
|-- tests
| -- Tests.fsproj
| -- Tests.fs
Where tests
contains the test project, you will install Fable.Mocha
into the Tests.fsproj
and it will look like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Tests.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\YourLibrary.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fable.Mocha" Version="2.16.0" />
</ItemGroup>
</Project>
then you can start writing your tests into the Test.fs
file:
module Tests
open Fable.Mocha
let arithmeticTests =
testList "Arithmetic tests" [
test "plus works" {
Expect.equal (1 + 1) 2 "plus"
}
test "Test for falsehood" {
Expect.isFalse (1 = 2) "false"
}
testAsync "Test async code" {
let! x = async { return 21 }
let answer = x * 2
Expect.equal 42 answer "async"
}
]
Mocha.runTests arithmeticTests |> ignore
Install the actual mocha
test runner as a dev dependency:
npm install --save-dev mocha
then set the package type to module
(otherwise, Mocha won't run), and add the following pretest
and test
npm scripts to your package.json
file:
{
"type": "module",
"scripts": {
"pretest": "dotnet fable tests -o dist/tests",
"test": "mocha dist/tests"
},
"devDependencies": {
"mocha": "^9.2.0"
}
}
Now you can simply run npm test
in your terminal and it will run the pretest
script to compile the test project and afterwards the test
script to actually run the (compiled) tests using mocha.
If you don't want to run a browser when running tests you should be aware of possible problems related to the compilation of CSS files in Mocha.
That could result in Unexpected token '.'
error.
To disable compilation of CSS files you should run the following command:
npm install --save-dev ignore-styles
and update test
npm script in package.json
file to the following:
"test": "mocha dist/tests --require ignore-styles"
Trying to use mocha to run tests in the browser will give you headaches as you have to include the compiled individual test files by yourself along with mocha specific dependencies. That's why Fable.Mocha includes a built-in test runner for the browser. You don't need to change anything in the existing code, it just works!
Compile your test project using default dotnet fable
then bundle the result into a bundle.js
file.
Then add an index.html
page inside directory called public
that contains:
<!DOCTYPE html>
<html>
<head>
<title>Mocha tests</title>
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>
Create a webpack config file that compiles your Tests.fsproj
var path = require("path");
module.exports = {
entry: {
// the compiled tests file as an entry point
app: "./tests/Tests.fs.js",
},
output: {
path: path.join(__dirname, "./public"),
filename: "bundle.js",
},
devServer: {
contentBase: "./public",
port: 8080,
}
}
Now you can run your tests live using webpack-dev-server or compile the tests and run them by yourself. Add these scripts to your package.json
"start": "dotnet fable tests --runFast webpack-dev-server",
Now if you run npm start
you can navigate to http://localhost:8080
to see the results of your tests.
The tests you write in the browser can be easily executed inside a headless browser such you can run them in your CI server. Using a simple console application, install the package Fable.MochaPuppeteerRunner
:
mkdir HeadlessTests
cd HeadlessTests
dotnet new console -lang F#
dotnet add package Fable.MochaPuppeteerRunner
Then change the contents of Program.fs
into:
[<EntryPoint>]
let main argv =
"../public"
|> System.IO.Path.GetFullPath
|> Puppeteer.runTests
|> Async.RunSynchronously
Where public
is the directory where the compiled tests, the bundle.js
file next to index.html
:
{repo}
|
|-- HeadlessTests
|-- HeadlessTests.fsproj
|-- Program.fs
|-- public
|-- index.html
|-- bundle.js
Also you need to add the RunWorkingDirectory
property to HeadlessTests.fsproj
as follows below:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6</TargetFramework>
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
</PropertyGroup>
Now simply dotnet run
and the tests will run inside the headless browser after the download finishes.
You can also add a npm build script to run the headless tests after compiling the Tests
project:
"test-headless": "webpack && dotnet run --project ./HeadlessTests/HeadlessTests.fsproj"
Remember to gitignore the directory of the downloaded chromium add .local-chromium
to your gitignore file.
Since the API exactly follows that of Expecto's you can simply run the tests on dotnet as well using Expecto. This way you can check whether your code runs correctly on different platforms whether it is dotnet or node.js. This is achieved using compiler directives as follows. First of all you need to install the Expecto
library from nuget:
dotnet add package Expecto
Then inside your Test.fs
file, you can the use the special FABLE_COMPILER
directive. This directive is active when Fable is compiling the project, if it is not active it means that the project being compiled using dotnet like any other dotnet application.
When Fable is compiling the project, you hide the dotnet specific code (i.e. using Expecto) and when dotnet is compiling the code, you hide the Fable specific code (i.e. using Fable.Mocha).
#if FABLE_COMPILER
open Fable.Mocha
#else
open Expecto
#endif
The same goes for the entry point:
[<EntryPoint>]
let main args =
#if FABLE_COMPILER
Mocha.runTests allTests
#else
runTestsWithArgs defaultConfig args allTests
#endif
And you are done, the code of the tests themselves doesn't need to change! Of course assuming you don't have platform specific code in there. This feature is made to test pure F# code that should give the same results with dotnet and Fable.
The function Mocha.runTests
can take nested test lists so you can group multiple test lists under a larger testList "All"
that combines all the tests
module Tests
open Fable.Mocha
let firstModuleTests =
testList "firstModule" [
test "module works properly" {
let answer = 21
Expect.areEqual 42 (answer * 2)
}
]
let secondModuleTests =
testList "secondModuleModule" [
test "module works properly" {
let answer = 31415.0
let pi = answer / 10000.0
Expect.areEqual 3.1415 pi
}
]
// Tests can be nested too!
let nestedTests =
testList "first level" [
testList "second level" [
testCase "my test code" <| fun _ -> ()
]
]
let allTests = testList "All" [
firstModuleTests
secondModuleTests
nestedTests
]
Mocha.runTests allTests
To run tests in succession in the browser, you can use testSequenced
as follows:
testSequenced <| testList "Sequential" [
testAsync "one" {
do! Async.Sleep 1000
}
test "sync one" {
Expect.isTrue true ""
}
testAsync "two" {
do! Async.Sleep 1000
}
test "sync two" {
Expect.isTrue true ""
}
testAsync "three" {
do! Async.Sleep 1000
}
]
The browser runner will make sure these tests are run in succession, one after another. The mocha runner in node.js runs the tests sequentially by default.
The build project inside ./build
has multiple build tasks, most important of these:
cd ./build
# run tests on node.js with mocha as test runner
dotnet run nodejs-test
# run tests on dotnet with expecto as test runner
dotnet run dotnet-test
# run tests inside a headless browser
dotnet run headless-test