Skip to content

Lightning-fast testing for a lightning-fast compiler

License

Notifications You must be signed in to change notification settings

dusty-phillips/rescript-zora

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

88 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rescript-zora: Lightning-fast unit tests

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.

If you've used older versions of this package

4 → 5

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.

3 → 4

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.

Installation

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"]

Suggested configuration

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"]
}

Stand-alone test

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.

Combining tests

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")
  })
})

Running tests in parallel (async)

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.

Combining parallel tests

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.

Test runner

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.

Skip, only, and fail

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'",

Assertions

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"))
})

Running in the browser

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.

Source Maps

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.

Contributing

PRs are welcome.

Releasing

This is for my reference

  • update the version in rescript.json
  • npx np