# 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 type Async<'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 what property 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: Generates ResizeArrays.
  • RandomValues.ListOf gen: Generates Lists.
  • 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 has None as a base value, then maps items using Some.