Unit testing with jest

I’d like to use a unit testing system that integrates nicely with the VS Code test framework, like Jest. From comments in this forum it seems like some people have been able to do this using some kind of compatibility system with the “other syntax” and bs-jest. Many people say they can’t get unit testing working at all and the ReScript team is creating something. When I search the package library on this web site for testing there is almost nothing. So I’m confused. Is there a straightforward way to do unit testing? If not how is it possible that Facebook, who started this whole thing, has “abandoned” ReScript to the point that their own testing system isn’t suppprted. I was really excited about ReScript until I realized that testing might be difficult and there is no “parser” library package - makes me wonder how well supported this project really is.

1 Like

I use simple interop for unit test with Jest
I can see it here: https://github.com/snatvb/re-fp

5 Likes

When you setup tests, you just have to decide whether you want to use ReScript with es6 modules or commonjs. Once you have chosen your preference, then you may have to add some small additional configuration to adjust Jest’s behavior. I think the most popular choice right now is:

  • use es6 output from Rescript
  • tell jest to use babel-jest
  • use transformIgnorePatterns to tell jest to NOT skip applying babel downgrading from es6 modules to commonjs for each compiled ReScript library

One way to accomplish above with create-react-app is to pass the following arguments to jest on the command line (replacing bs-fetch with whatever libraries apply to your project, as noted in bsconfig dependencies):

     yarn run test \
		--env=jsdom \
		--transformIgnorePatterns "node_modules/(?!(bs-platform|bs-fetch)/)" 

create-react-app already enables babel-jest, so you don’t need to set the transform configuration.

I haven’t confirmed that the example mentioned above works exactly with create-react-app, since I am using react-app-rewired currently, but it should be close.


Alternatively, this repo also shows my WIP attempt at using es6 output without resorting to using babel-jest: kanishka/rescript-webpack-jest-starter - rescript-webpack-jest-starter - Codeberg.org.


I agree overall that a starter without jest configured to run unit tests and react-testing-library tests is really an incomplete starter, and it looks poorly on rescript that most of the starters don’t present jest completely configured currently. I think that will change pretty soon as more starters are created and more users adopt rescript.

1 Like

Thanks. Will try it soon. On airplane now. Using it with create react app.

Was able to get it to kind-of work. First, I noticed from @snatvb that he wrote his own interop layer. At the moment it is very limited. Not much more than…

@val external describe: (string, @uncurry (unit => unit)) => unit = "describe"
@val external test: (string, @uncurry (unit => unit)) => unit = "test"
type e
@val external expect: 'a => e = "expect"
@send external toBe: (e, 'a) => unit = "toBe"
@send external toEqual: (e, 'a) => unit = "toEqual"

There is some stuff in there about promises I left out; didn’t understand and know the promise bindings are in flux. The other example from @kanishka used bs-jest, which has the parameters in the older data-last pattern, so you have to use the |> pipe instead which I didn’t like. So I’m left wondering if there is an official, new ReScript binding for jest. Would be really helpful if this exists.

Second, this is used with create react app and the built-in jest runner and new VS Code test manager in VS Code somehow finds my tests like the default App.test.tsx. But if I set the “jest” key in package.json - which is not there at all by default - as shown below (which does not work) the test runner will find my ReScript tests but can no longer find the App.test.tsx. I couldn’t figure out how to configure the testMatch key to find all my tests. In the end I had to eliminate the testMatch key and reconfigure bsconfig.json to output ReScript files as .js rather than .bs.js so it will always find tests matching .test.js

  "jest": {
    "transformIgnorePatterns": [
      "node_modules/(?!(bs-platform)/)"
    ],
   "testMatch": [
      "**/*.test.bs.js",
      "**/*.test.js"
    ],
  }

So overall it seems like using Jest with CRA is possible and probably not that hard. It is technically a bit beyond me to work out all the details. It would be really helpful if:

  1. There were official new ReScript bindings for Jest. I’m really surprised this does not exist given that Facebook started this whole thing.
  2. The ReScript Installation pages gave a step-by-step for how to enable Jest with a CRA project. I’d like to be able to keep the .bs.js extension.
  3. There was some way to put a module test file in the same folder as the tests without ugly naming. If you’ve got both a Math.res and Math.test.res, then the code inside the test file can’t find the code in the module you’re trying to test. I had to name the test file like MathTest.test.res. Maybe a module name consists of everything before the first period in a file name.

=== UPDATE ===

Seems to be working pretty well now. Read this: jest and create-react-app. So to get Jest working with ReScript and a typescript create-react-app starter just took the following steps: (1) create a __tests__ folder and put my tests in there. (2) create a minimal Jest.res interop file - see above - and put it somewhere other than the tests folder, and (3) update package.json with this (don’t understand yet). The tests automatically run and show up in the new VS Code test explorer. When I go to source on a test it takes me to the .bs.js file, not the .res, which is ok.

  "jest": {
    "transformIgnorePatterns": [
      "node_modules/(?!(bs-platform)/)"
    ]}

A simple test file…

open Jest

describe("math", () => {
  test("can add", () => {
    let result = 1 + 2
    result->expect->toEqual(3)
  })
})
3 Likes

And if anybody wants to use this minimal interop with Node.js and es6 modules with .mjs extension you can add this to your package.json in your scripts and jest sections (modify as needed based on your setup):

"scripts": {
  ...
  "test": "NODE_OPTIONS=--experimental-vm-modules jest",
  "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watchAll"
}

"jest": {
  ...
  "testMatch": ["**/__tests__/*.?(m)js"],
  "moduleFileExtensions": ["js", "mjs"]
}
2 Likes

I spoke too soon. I ran into problems trying to run test where code used some of the ReScript modules, like Pervasives.int_of_string. Jest was giving me an error Cannot use import statement outside a module. I don’t totally understand this - something about Jest not supporting es6 modules. There must be lots of es6 module usage in React apps so I don’t understand what is so special about the ReScript stuff that requires extra attention. In my case here I was calling the ReScript functions only from my test files, not from my main project. Also, I wasn’t sure about that transformIgnorePatterns with bs-platform because I didn’t see that folder on my computer. So instead, I did NOT add a jest configuration section to package.json. Instead, I updated the test script as follows and it seems to work using the Jest extension that automatically runs tests and shows results in the test explorer AND when I manually run the test script.

 "test": "react-scripts test ---transformIgnorePatterns \"node_modules/(?!((bs-platform)|(rescript))/)\"",

So the steps that work for me (so far) to modify a vanilla create-react-app with TypeScript template are:

  1. Create a __tests__ folder and put my .res tests in there.
  2. Create a minimal Jest.res interop file - see above - and put it somewhere other than the tests folder.
  3. Add the transformIgnorePatterns stuff to the test script in package.json.
1 Like

I am not certain here, but I understand the difference as follows. You may see es6 module usage in two places.

  1. A library may use es6 modules in its source files. If it is a regular NPM package, I assume that the authors apply babel before publishing to NPM.
  2. The source code in your application may use es6 modules. In that case, I believe jest will apply babel-jest to your source code by default.

I think ReScript libraries that are published to npm are published as ReScript source files. Then, you locally compile those files into either es6 modules or commonjs modules, depending on your bsconfig settings. If you choose es6 module output, then you need to ensure babel-jest downgrades the javascript output of your ReScript libraries, which is done with the appropriate transformIgnorePatterns value.

You can ignore my earlier comment about using a cli argument to avoid ejecting. The create-react-app docs mention supporting the transformIgnorePatterns configuration key, as you probably already noticed.

I expect using package.json’s jest key and using a cli argument should behave the same for transformIgnorePatterns. The value gets overwritten: https://github.com/facebook/create-react-app/blob/b45ae3c9caf10174d53ced1cad01a272d164f8de/packages/react-scripts/scripts/utils/createJestConfig.js#L100.

As for bs-platform, that was the old name for the rescript package, which is why you don’t see it locally.

I haven’t figured out how to do a binding for a testAll as Jest’s test.each() looks like it would be pretty complex based on the code. But something like this seems to work in a pinch if you need to run the same test for multiple data inputs:

list{(1, 1, 2), (1, 2, 3), (2, 3, 5)}->Belt.List.forEach(((a, b, expected)) =>
    test(j`add($a, $b)`, () => {
      expect(a + b)->toBe(expected)
    })
  )

This is what I use. The test.each is done via a hack; I don’t bother to strongly type the test part
and do a send for each method on that object. Instead I just treat test.each as a single unit.
Works fine for me.

@val external describe: (string, @uncurry (unit => unit)) => unit = "describe"
@val external test: (string, @uncurry (unit => unit)) => unit = "test"
@val external _each1: (array<'a>, . string, 'a => unit) => unit = "test.each"
@val external _each2: (array<('a, 'b)>, . string, ('a, 'b) => unit) => unit = "test.each"
@val external _each3: (array<('a, 'b, 'c)>, . string, ('a, 'b, 'c) => unit) => unit = "test.each"
@val external _each4: (array<('a, 'b, 'c, 'd)>, . string, ('a, 'b, 'c, 'd) => unit) => unit = "test.each"

// Helper methods that seem easier to use for me. Also automatically makes the
// title of the tests include the test index and parameters. Make sure to put ->
// ignore at the end of using these; if not sometimes there are warnings/errors
// in the javascript.
let testEach = (title, data, f) => _each1(data)(. `${title}(%#) %p`, f)
let testEach2 = (title, data, f) => _each2(data)(. `${title}(%#) %p %p`, f)
let testEach3 = (title, data, f) => _each3(data)(. `${title}(%#) %p %p %p`, f)
let testEach4 = (title, data, f) => _each4(data)(. `${title}(%#) %p %p %p %p`, f)

type m<'a> // matcher of type 'a
@val external expect: 'a => m<'a> = "expect"
@send external toBe: (m<'a>, 'a) => unit = "toBe"
@send external toEqual: (m<'a>, 'a) => unit = "toEqual"
@send external toThrow: m<'a> => unit = "toThrow"
@get external expectNot: m<'a> => m<'a> = "not"

4 Likes

@jmagaram this is perfect and the helper functions are a bonus as they are my preferred way as well. :+1: :+1:

Is there any need to @uncurry the callbacks that have only one arg?