This package provides ReScript bindings for the Zora testing framework. ReScript and Zora go very well together because they have a common mission of SPEED.
In the interest of maintaining that speed, this package is asynchronous by default, though you can create blocking tests if you prefer.
This package mostly just binds directly to Zora, but there are a couple niceties to help work with ReScript promises and the standard library.
The code was updated to ReScript 11.
Nearly all check functions have gained an optional ~msg
argument for passing
in the message for the check. This makes the message optional, defaulting to
what Zora provides as a message.
The resultError
function now accepts a check function to verify the value
contained by the Error
.
There is a new ignoreValue
check function to pass to the option*
and
result*
functions when the values are inconsequential and the type of variant
is the purpose of the test.
I've migrated everything to async/await syntax and it now requires
ReScript 10.1. You'll need to convert any non-blocking tests in your
existing codebase to return promise or define them with async, but
you don't need to throw done()
calls in all your async tests.
Note: If you don't have a ReScript 9.1.1 project initialized already, the
fastest way to get one is with npx rescript init myproject
.
Install zora and this package:
npm install --save-dev @dusty-phillips/rescript-zora
Add @dusty-phillips/rescript-zora
as a dependency in your rescript.json
:
"bs-dev-dependencies": ["@dusty-phillips/rescript-zora"]
Recent versions of node seem to cooperate better if you explicitly use the .mjs or
.cjs suffix for your files. So you'll want your rescript.json
to contain either:
- suffix:
.mjs
and module:esmodule
- suffix:
.cjs
and module:commonjs
I use .mjs
in this configuration, but I have tested it with .cjs
and it seems
to work.
You'll probably also want to add the following package-specs
configuration to
your rescript.json
:
"suffix": ".mjs",
"package-specs": {
"module": "esmodule",
"in-source": true
},
If you like to keep your tests separate from your source code, you'll need to add that directory so ReScript will compile your test files:
"sources": [
{
"dir": "src",
"subdirs": true
},
{ "dir": "tests", "subdirs": true, "type": "dev" }
],
So a minimal rescript.json
might look like this:
{
"name": "myproject",
"version": "2.0.0",
"suffix": ".mjs",
"sources": [
{
"dir": "src",
"subdirs": true
},
{ "dir": "tests", "subdirs": true, "type": "dev" }
],
"package-specs": {
"module": "esmodule",
"in-source": true
},
"bs-dev-dependencies": ["@dusty-phillips/rescript-zora"]
}
The simplest possible Zora test looks like this:
// tests/simple.test.res
open Zora
zoraBlock("should run a test synchronously", t => {
let answer = 3.14
t->equal(answer, 3.14, ~msg="Should be a tasty dessert")
})
Building this with ReScript will output a tests/simple.mjs
file that
you can run directly with node
:
╰─○ node tests/standalone.js
TAP version 13
# should run a test asynchronously
ok 1 - Should answer the question
# should run a test synchronously
ok 2 - Should be a tasty dessert
1..2
# ok
# success: 2
# skipped: 0
# failure: 0
This output is in Test Anything Protocol format. The zora docs go into more detail on how it works with Zora.
You can include multiple zoraBlock
statements, or you can pass the t
value
into the block
function:
open Zora
zoraBlock("Should run some simple blocking tests", t => {
t->block("should greet", t => {
t->ok(true, ~msg="hello world")
})
t->block("should answer question", t => {
let answer = 42
t->equal(answer, 42, ~msg="should be 42")
})
})
The Block
in zoraBlock
indicates that this is a blocking test. It's faster
to run multiple independent tests in parallel:
// tests/standaloneParallel.res
open Zora
zora("should run a test asynchronously", async t => {
let answer = 42
t->equal(answer, 42, ~msg="Should answer the question")
})
zora("should run a second test at the same time", async t => {
let answer = 3.14
t->equal(answer, 3.14, ~msg="Should be a tasty dessert")
})
Note the absence of zoraBlock
, and the presence of async
. You can
await other promises inside the test if you want.
You can nest parallel async tests inside a blocking or non-blocking test, and run blocking tests alongside parallel tests:
// parallel.test.res
open Zora
let wait = (amount: int) => {
Js.Promise2.make((~resolve, ~reject) => {
reject->ignore
Js.Global.setTimeout(_ => {
resolve(. Js.undefined)
}, amount)->ignore
})
}
zora("Some Parallel Tests", async t => {
let state = ref(0)
t->test("parallel 1", async t => {
{await wait(10)}-> ignore
t->equal(state.contents, 1, ~msg="parallel 2 should have incremented by now")
state.contents = state.contents + 1
t->equal(state.contents, 2, ~msg="parallel 1 should increment")
})
t->test("parallel 2", async t => {
t->equal(state.contents, 0, ~msg="parallel 2 should be the first to increment")
state.contents = state.contents + 1
t->equal(state.contents, 1, ~msg="parallel 2 should increment")
})
t->test("parallel 3", async t => {
{await wait(20)}->ignore
t->equal(state.contents, 2, ~msg="parallel 1 and 2 should have incremented by now")
state.contents = state.contents + 1
t->equal(state.contents, 3, ~msg="parallel 3 should increment last")
})
})
This is the default and preferred test setup (zora and test) to take advantage of
parallelism for speed. Note that you can combine parallel and blocking tests
in the same zora
or zoraBlocking
block as well.
You probably don't want to run each of your test files using separate node
commands, though. You can use any TAP compliant test runner (see
here for a list), but your best
bet is probably to use Zora's bundled
pta runner with
onchange for watching for file changes:
npm install --save-dev pta onchange
With these installed, you can set the test
command in your scripts
as follows:
"test": "onchange --initial '{tests,src}/*.js' -- pta 'tests/*.test.js'",
Or, if you prefer to keep your tests alongside your code in your src
folder:
"test": "onchange --initial 'src/*.js' -- pta 'src/*.test.js'",
Now npm test
will do what you expect: run a test runner and watch for file
changes.
Zora exposes functions to skip tests if you need to. If you have a failing
test, just replace the call to Zora.test
with a call to Zora.skip
. Or, if
you're running blocking tests, replace Zora.block
with Zora.blockSkip
.
For example:
open Zora
zora("should skip some tests", t => {
t->skip("broken test", t => {
t->fail(~msg="Test is broken")
})
t->blockSkip("also broken", t => {
t->fail(~msg="Test is broken, too")
})
})
The above also illustrates the use of the Zora.fail
assertion to force a test
to be always failing.
If you want to run and debug a single test, you can run it in only
mode. As
with skip, change the test's name from test
to only
or block
to
blockOnly
. You must also change the top level zora
/zoraBlock
to
zoraOnly
/zoraBlockOnly
.
open Zora
zoraOnly("should skip some tests", t => {
t->only("only run this test", t => {
t->ok(true, ~msg="Only working test")
})
t->test("don't run this test", t => {
t->fail(~msg="Test is broken")
})
})
However, only
tests are intended only in development mode and zora will fail
by default if you try to run one. To run in only mode, you can run:
npm test -- --only
or
ZORA_ONLY=true npm test
If you use this feature a lot, you could also consider putting additional test
commands in your package.json
scripts, one for local only development and one
for CI:
"test": "onchange --initial '{tests,src}/*.js' -- pta 'tests/*.test.js'",
"test:only": "onchange --initial '{tests,src}/*.js' -- pta --only 'tests/*.test.js'",
"test:ci": "pta 'tests/*.test.js'",
This library models all the default assertions provided by Zora except for
those dealing with raising exceptions, which don't map neatly to ReScript
exceptions. There are additional bindings for checking if a ReScript option
is Some()
or None
or if a Result
is Ok()
or Error()
and asserting
on the value therein (except for None
as there is no value to check). A
ignoreValue
function is provided in those instances where asserting on the
value is unimportant.
In the interest of avoiding bloat, I do not intend to add a lot of other ReScript-specific assertions.
//tests/assertions.test.res
open Zora
zora("Test assertions", t => {
t->equal(42, 42, ~msg="Numbers are equal")
t->notEqual(42, 43, ~msg="Numbers are not equal")
let x = {"hello": "world"}
let y = x
let z = {"hello": "world"}
t->is(x, x, ~msg="object is object")
t->is(x, y, ~msg="object is object")
t->isNot(x, z, ~msg="object is not object with same values")
t->equal(x, z, ~msg="Object is deep equal")
t->ok(true, ~msg="boolean is ok")
t->notOk(false, ~msg="boolean is not ok")
t->optionNone(None, ~msg="None is None")
t->optionSome(Some(x), (t, n) => t->equal(n["hello"], "world", ~msg="option should be hello world"))
t->resultError(Error(x), (t, n) => t->equal(n["hello"], "world", ~msg="Is Error Result"))
t->resultOk(Ok(x), (t, n) => t->equal(n["hello"], "world", ~msg="Is Ok Result"))
})
Zora supports running tests in the browser, but I have not tested it with this ReScript implementation. I am open to PRs that will make this ReScript implementation work in the browser if changes are required.
The biggest problem with this library is that test failures point to the lines in the compiled js files instead of ReScript itself. If someone knows how to configure ReScript and zora to use source maps, I'd love a PR.
PRs are welcome.
This is for my reference
- update the version in
rescript.json
npx np