#
Unit testing with WebSharper.Testing
- NuGet package:
WebSharper.Testing
- DLL:
WebSharper.Testing.dll
- Namespace:
WebSharper.Testing
WebSharper.Testing
provides an easy-to-use, concise syntax for writing unit tests for libraries, services and web applications. It's a wrapper around QUnit, which handles synchronous and asynchronous expressions and generates random data for property testing. The main tests page is meant to be run in a browser, but server-sides can also be tested by making remote calls from the tests.
The code samples below assume that the module or assembly containing them is annotated with [<JavaScript>]
, except when they refer to server-side code.
#
Test 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 inside is not executed immediately, but only when it is passed to Runner.RunTests
.
let MyTests =
TestCategory "MyTests" {
Test "Equality" {
equal 1 1
}
}
#
Test
Creates a definition for a single named test. Similarly to TestCategory
, it delays the execution of its content and registers it in QUnit
.
let EqualityTests() =
Test "Equality" {
equal 1 1
notEqual 1 2
}
Test "name"
is a computation expression (CE) builder that defines various custom operations (such as equal
, notEqual
, etc.) to use for individual assertions.
#
Runner.RunTests
Takes an array of TestCategory
values, and returns an IControlBody
, which can be used inside the client <@ ... @>
helper to serve a page that runs the given tests. You can also call its ReplaceInDom
method directly on the client, to replace a placeholder DOM element with the content generated by QUnit
.
let RunAllTests() =
Runner.RunTests [|
MyTests
|]
Later, in your server-side code, you can serve this on a sitelet endpoint:
Content.Page(
Title = "Unit tests",
Body = client <@ RunAllTests() @>
)
#
Expect
Tells the testing framework how many test cases are expected to be registered for a given category. If a category registers no tests, QUnit
reports it as a failure, unless you add expect 0
.
let MyTests =
TestCategory "MyTests" {
Test "Equality" {
expect 2
equal 1 1
notEqual 1 2
}
Test "Not failing" {
expect 0
// A call to some outside function:
doNotFail()
// If this throws an error, that still fails the test case, otherwise OK.
}
}
#
Assertions
Most assertions have three additional variants:
xxxMsg
- takes an additional name argument for its assertion,xxxAsync
- takes a value of typeAsync<'T>
instead of the actual value'T
. Expected value is still provided as a value of'T
.xxxMsgAsync
- the combination of the above.
#
isTrue
Checks if a single argument expression evaluates to true
.
Test "Equality" {
isTrue (1 = 1)
isTrueMsg (1 = 1) "One equals one"
isTrueAsync (async { return 1 = 1 })
isTrueMsgAsync (async { return 1 = 1 }) "One equals one, async version"
}
#
isFalse
The negation of isTrue
: the test passes if the expression evaluates to false
.
Test "Equality" {
isFalse (1 = 2)
isFalseMsg (1 = 2) "One equals two is false"
isFalseAsync (async { return 1 = 2 })
isFalseMsgAsync (async { return 1 = 2 }) "One equals two is false, async version"
}
#
Equality checks
#
equal
Checks if two expressions evaluate to equal values when tested with WebSharper's equality semantics. This is the same as using the =
operator from F# code, unions and records have structural equality and overrides on the Equals
method or implementing IEquatable
are honored.
Test "Equality" {
equal (Some 1) (Some 1)
equalMsg (Some 1) (Some 1) "Option equality"
equalAsync (async { return Some 1 }) (Some 1)
equalMsgAsync (async { return Some 1 }) (Some 1) "Option equality, async version"
}
#
notEqual
The negation of equal
: fails if two values are equal with respect to WebSharper's equality.
#
jsEqual
Checks if two expressions evaluate to equal values when tested with JavaScript's ==
operator. This is the same as using the ==.
operator in F# code (available with open WebSharper.JavaScript
).
Test "Equality" {
jsEqual 1 1
jsEqualMsg 1 1 "One equals one"
jsEqualAsync (async { return 1 }) 1
jsEqualMsgAsync (async { return 1 }) 1 "One equals one, async version"
}
#
notJsEqual
The negation of jsEqual
: fails if two values are equal with JavaScript's ==
operator.
#
strictEqual
Checks if two expressions evaluate to equal values when tested with JavaScript's ===
operator. This is the same as using the ===.
operator in F# code (available with open WebSharper.JavaScript
).
#
notStrictEqual
The negation of strictEqual
: fails if two values are equal with JavaScript's ===
operator.
#
deepEqual
Checks if two expressions evaluate to equal values when tested with QUnit
's deepEqual
function. This uses a deep recursive comparison, working on primitive types, arrays, objects, regular expressions, dates and functions.
#
notDeepEqual
The negation of deepEqual
: fails if two values are equal using QUnit
's notDeepEqual
function.
#
propEqual
Checks if two expressions evaluate to equal values when tested with QUnit
's propEqual
function. This function compares only the direct properties on an object with strict equality (===
).
#
notPropEqual
The negation of propEqual
: fails if two values are equal using QUnit
's notPropEqual
function.
#
approxEqual
Compares floating point numbers, where a difference/epsilon of < 0.0001
is accepted.
#
notApproxEqual
The negation of approxEqual
: fails if the difference of two values is < 0.0001
.
#
Exception testing
#
raises
Checks that an expression is raising any exceptions, and passes test if it does. Note that the arguments are always implicitly enclosed within a lambda function by the framework, making sure that they are only evaluated when the test is running.
Test "Exceptions" {
raises (failwith "should fail")
raisesMsg (failwith "should fail") "Failure is expected"
raisesAsync (async { failwith "should fail" })
raisesMsgAsync (async { failwith "should fail" }) "Failure is expected from inside async"
}
#
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.
Test "Equality" {
equal 1 1
let! one = async { return 1 }
equal one 1
}
#
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 register tests, as its intended use is to create sub-tests that can be executed inside a named test when doing property testing.
let SubTest x =
Do {
equal x x
}
#
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
composed from these. Using the obj
type results in values from a mix of various types. When using a non-supported type, it results in a compile-time error.
Test "Equality on ints is reflexive" {
property (fun (x: int) ->
Do {
equal x x
}
)
}
#
propertyWith
Similar to property
, but the generator logic is not inferred from a type, but instead is passed as an RandomValues.Generator
argument, which is described later on this page.
There are also constructors and combinators in the Random
module to get Generator
values, allowing easier composition of complex test data.
Test "Equality on ints is reflexive" {
propertyWith RandomValues.Int (fun (x: int) ->
Do {
equal x x
}
)
}
#
propertyWithSample
Similar to propertyWith
, but the argument is an exact sample on which the property is tested. See RandomValues.Sample
below.
Test "Equality on ints is reflexive" {
propertyWithSample (RandomValues.Sample [| 1; 2; 3 |]) (fun (x: int) ->
Do {
equal x x
}
)
}
#
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, and you can use Do
to define the body of the loop.
Test "Equality on ints is reflexive" {
forEach { 1 .. 3 } (fun x ->
Do {
equal x x
}
)
}
#
Sample 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 follows:
// module Random
type Generator<'T> =
{
/// An array of values that must be tested.
Base: 'T []
/// A function generating a new random value.
Next: unit -> 'T
}
#
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.
Examples:
let sampleFromArray = RandomValues.Sample([| 1; 2; 3 |]);
let sampleFromGenerator = RandomValues.Sample(RandomValues.Int); // creates 100 values
let sampleFromGenerator10 = RandomValues.Sample(RandomValues.Int, 10); // creates 10 values
let sampleInferred = RandomValues.Sample<int>();
let sampleInferred10 = RandomValues.Sample<int>(10); // creates 10 values
#
Random generator constructors
The following 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 as whatproperty
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
: Filters 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
: GeneratesResizeArray
s.RandomValues.ListOf gen
: 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
.