Skip to content

johnelse/ocaml-webtest

Repository files navigation

ocaml-webtest

Build status API reference

A unit test framework, fully compatible with js_of_ocaml, and written with in-browser testing of JavaScript code in mind.

Heavily influenced by oUnit.

You may find this helpful if you want to

  • test OCaml bindings to a JavaScript library
  • write tests for a JavaScript library compiled from OCaml

You could even use this library to test normal OCaml code, but in that case you're probably better off just using oUnit for the extra features it provides.

Library contents

ocaml-webtest consists of two libraries:

  • webtest

This has no dependencies, and contains code for creating tests and suites.

  • webtest-js

This depends on js_of_ocaml, and contains code used for running tests in a browser.

Creating test suites

ocaml-webtest supports two kinds of test cases - synchronous and asynchronous.

Both kinds of test cases can use the assertion functions assert_true, assert_equal and assert_raises to check for expected behaviour.

Synchronous test cases are functions of type unit -> unit, and in order to pass should return cleanly without throwing an exception.

Some examples of simple synchronous test cases:

let sync_test1 () = Webtest.Suite.assert_equal (get_five ()) 5

let sync_test2 () = Webtest.Suite.assert_true (get_value ())

let sync_test3 () = Webtest.Suite.assert_raises MyExn (exception_thrower ())

Asynchronous test cases are functions of type ((unit -> unit) -> unit) -> unit. When run they are passed a wrapper function which must be used to wrap any asynchronous code which should be triggered as part of the test. In order to pass, an asynchronous test case should not only return cleanly, it should also make sure that the wrapped code runs successfully. Asynchronous test cases can be used to check that an event handler associated with a JavaScript object has been called.

An example of an asynchronous test case:

let async_test wrapper =
  let js_object = create_object () in

  js_object##onclose :=
    Dom_html.handler (fun _  ->
      wrapper (fun () ->
        assert_true "Object has been closed" (is_closed js_object));
      Js._false);

  js_object##close

If you don't need to perform any assertions in the asynchronous code but just need to check that a handler fired, you can call the wrapper function with Async.noop, which is just an alias for fun () -> ().

Synchronous and asynchronous test cases can be combined into suites using the functions >::, >:~ and >::: - for example:

open Webtest.Suite

let suite =
  "suite" >::: [
    "sync_test1" >:: sync_test1;
    "sync_test2" >:: sync_test2;
    "sync_test3" >:: sync_test3;
    "async_test" >:~ async_test;
  ]

In-browser testing

Once you've created a suite, you can integrate it into an HTML document using Webtest_js.Runner.setup:

let () = Webtest_js.Runner.setup suite

This will create the global JavaScript object webtest which exposes a simple API for running the test suite.

  • webtest.run is a function with no arguments - calling it will run the test suite.
  • webtest.finished is a boolean indicating whether the suite run has finished.
  • webtest.passed is a boolean indicating whether all the tests passed.
  • webtest.log contains the log produced by running the tests.

This API can be used by browser automation tools such as Selenium WebDriver. For an example implementation in Python, see test_driver.py.

A "known good" setup for automating browser tests is:

  • Firefox 89
  • Python 3.8.5 with version 3.141.0 of the Selenium bindings
  • geckodriver 0.29.1
  • Xvfb (optional, unless running on a headless machine)

Examples

ocaml-webtest tests itself in both OCaml (run_tests_ocaml.ml), in JavaScript (run_tests_browser.ml) and in Node.js (run_tests_nodejs.ml).

For a more real-world example, see ocaml-webaudio.