Unit testing
The WebSharper.Testing
library (available on NuGet) exposes a clean F# syntax to write unit tests for libraries, services and websites.
It is a wrapper around QUnit
for presentation which provides transparent handling of synchronous and asynchronous expressions, and generating random data for property testing.
The tests page is meant to be ran in a browser, but the server side of a website can be tested too by making remote calls from the tests.
All functionality within are accessible with the WebSharper.Testing
namespace.
Code samples below are also assuming that the module or assembly containing them is annotated with [<JavaScript>]
except
when referring to being server-side code.
Categories and tests
TestCategory
Creates a definition for a single named test category.
You can think of it as similar to F#'s lazy
keyword: the code body inside it is not executed immediately,
but when only when it will be passed to Runner.RunTests
.
So the returned value (of type TestCategory
) need to be assigned to local or module variable if you want to run it later in your project.
Example:
Test
Creates a definition for a single named test.
Similar to TestCategory
, it delays the execution of its content.
Does not return anything, but registers tests in QUnit
.
Do not evaluate it in a module context if you want the test to be under a category reliably.
Example:
Test "name"
is a computation expression builder, which defines many named custom operations to use for
individual assertions.
These all have four versions for optionally naming it and/or using it with an asynchronous argument:
- operations with
Msg
also accept a name for that single assertion, - operations with
Async
accept a value of typeAsync<'T>
as the actual value instead. Expected value is still provided as a value of'T
.
Skipped, todo, and conditional tests
Use the Skip
function instead of Test
to skip a test case.
This is useful alternative to commenting out the test code, to still have it visible in the test runner.
You can also use Todo
to mark a test as not fully passing yet.
QUnit will show it as passing until there is at least one assertion failing or there is an exception.
If the test passes, it will be shown failing to signal that it can now be changed to a regular test.
All the test builders have an ...If
variant that also takes a boolean condition.
When the condition is false
, the test is fully skipped.
Runner.RunTests
Takes an array of TestCategory
values
Returns an IControlBody
, which can be used inside the client <@ ... @>
helper to serve a page that runs the given tests,
or call its ReplaceInDom
method directly on the client, to replace a placeholder DOM element to the content generated by QUnit
.
Example:
Later, in server-side code, serve this on a sitelet endpoint:
Expect
Tells the testing framework how many test cases are expected to be registered for this category.
If the category would register no tests, QUnit
reports it as a failed test case, unless you add expect 0
.
Example:
Assertions
isTrue
Checks if a single argument expression evaluates to true
.
Example:
isFalse
The negated versions of the above, the test passes if the expression evaluates to false
.
Example:
Equality checks
equal
Checks if the two expressions evaluate to equal values as tested with WebSharper's equality.
This is the same as using the =
operator from F# code, unions and records have structural equality
and overrides on Equals
method or implementing IEquatable
is respected.
Example:
notEqual
The negated version of the above, fails if values are equal with WebSharper's equality.
jsEqual
Checks if the two expressions evaluate to equal values as tested with JavaScript's ==
operator.
This is the same as using the ==.
operator in F# code (available with open WebSharper.JavaScript
),
which is directly translated to ==
in JavaScript.
Example:
notJsEqual
The negated version of the above, fails if values are equal with JavaScript's ==
operator.
strictEqual
Checks if the two expressions evaluate to equal values as tested with JavaScript's ===
operator.
This is the same as using the ===.
operator in F# code (available with open WebSharper.JavaScript
),
which is directly translated to ===
in JavaScript.
notStrictEqual
The negated version of the above, fails if values are equal with JavaScript's ===
operator.
deepEqual
Checks if the two expressions evaluate to equal values as tested with QUnit
's deepEqual
function
which is a deep recursive comparison, working on primitive types, arrays, objects, regular expressions, dates and functions.
notDeepEqual
The negated version of the above, uses QUnit
's notDeepEqual
function instead.
propEqual
Checks if the two expressions evaluate to equal values as tested with QUnit
's propEqual
function
which compares just the direct properties on an object with strict equality (===
).
notPropEqual
The negated version of the above, uses QUnit
's notPropEqual
function instead.
approxEqual
Compares floating point numbers, where a difference of < 0.0001
is accepted.
notApproxEqual
The negated version of the above, fails if the difference of the two values is < 0.0001
.
Exception testing
raises
Checks that the expression argument is raising any exception, passes test if it does. Note that actual value arguments are always implicitly enclosed within a lambda function, so don't need to pass a function explicitly to make sure that the value is only evaluated when the test are running.
Example:
Asynchronous tests
Test
computation expressions also allow binding an async
workflow at any point.
If this is not used, and all assertions are synchronous then the entire test case will
run synchronously for optimal performance.
Example:
Property testing
Do
Using Do
is very similar to using Test "Name"
, it is a computation expression builder, enabling the same custom operations.
The difference is that it is not named, and also by itself does not registers tests,
as it's intended use is to create sub-tests, that can be executed inside a named test when doing property testing.
Example:
property
Auto-generates 100 random values based on a type and runs a sub-test with all of them.
Supported types are int
, float
, bool
, string
, unit
, obj
and also
tuples, lists, arrays, options, seq
and ResizeArray
made from these.
Using the obj
type results in values from mix of various types.
When using a non-supported type, it results in a compile-time error.
Example:
propertyWith
Similar to above, but the generator logic is not inferred from type, but passed to the operation.
It takes as first argument a record value of type RandomValues.Generator
, which is described here.
There are also constructor functions and combinators in the Random
module to get Generator
values,
allowing easier composition of complex testing values.
Example:
propertyWithSample
Similar to above, but the argument is an exact sample on which the property is tested. See RandomValues.Sample below.
Example:
Looping
forEach
You cannot use a regular for
loop inside a Test
computation expression, but you can emulate it with the forEach
operation.
Its use is similar to property testing, you can use Do
to define the body of the loop.
Example:
Sample value generators
RandomValues.Generator
A generator is a record that can hold an array of static values to always test against, and a function that can return additional values to be tested dynamically. It is defined as such:
RandomValues.Sample
The RandomValues.Sample
type is a thin wrapper around an array of values, exposing multiple constructors
to create from a given array or explicit or inferred generators.
Example:
Random generator constructors
The following values and function produce simple RandomValues.Generator
values:
RandomValues.StandardUniform
: Standard uniform distribution sampler.RandomValues.Exponential
: Exponential distribution sampler.RandomValues.Boolean
: Generates random booleans.RandomValues.Float
: Generates random doubles.RandomValues.FloatExhaustive
: Generates random doubles, including corner cases (NaN, Infinity).RandomValues.Int
: Generates random int values.RandomValues.Natural
: Generates random natural numbers (0, 1, ...).RandomValues.Within low hi
: Generates integers within a range.RandomValues.FloatWithin low hi
: Generates doubles within a range.RandomValues.String
: Generates random strings.RandomValues.ReadableString
: Generates random readable strings.RandomValues.StringExhaustive
: Generates random strings including nulls.RandomValues.Auto<'T>
: Auto-generate values based on type, same asproperty
uses internally.RandomValues.Anything
: Generates a mix of ints, floats, bools, strings and tuples, lists, arrays, options of these.
Random generator combinators
RandomValues.Map mapping gen
: Maps a function over a generator.RandomValues.SuchThat predicate gen
: Filter the values of a generator by a predicate.RandomValues.ArrayOf gen
: Generates arrays (up to lengh 100), getting the items from the given generator.RandomValues.ResizeArrayOf gen
: Similar to as above, generatesResizeArray
s.RandomValues.ListOf gen
: Similar to as above, generatesList
s.RandomValues.Tuple2Of (a, b)
: Generates a 2-tuple, getting the items from the given generators.RandomValues.Tuple3Of (a, b, c)
: Same as above for 3-tuples.RandomValues.Tuple4Of (a, b, c, d)
: Same as above for 4-tuples.RandomValues.OneOf arr
: Generates values from a given array.RandomValues.Mix a b
: Mixes values coming from two generators, alternating between them.RandomValues.MixMany gens
: Mixes values coming from an array of generators.RandomValues.MixManyWithoutBases gens
: Same as above, but skips the constant base values.RandomValues.Const x
: Creates a generator that always returns the same value.RandomValues.OptionOf x
: Creates a generator hasNone
as a base value, then maps items usingSome
.