Skip to content

Jest (testing)

Created: 2018-03-05 11:02:55 -0800 Modified: 2020-02-05 09:08:19 -0800

Installation:

  • Install dev dependency packages. For me, these were
    • babel-jest
    • eslint
    • eslint-plugin-jest
    • jest
    • If you didn’t already have eslint:
      • eslint-plugin-import
    • If you don’t already have it, Babel itself:
      • babel-plugin-transform-object-rest-spread
      • babel-preset-env
  • I didn’t have a babelrc, so I needed to make one. Also, because I export already-transpiled ES6 classes in @botland/shared and ran into issues originally with trying to extend those classes, so I needed to add “exclude”: [“transform-es2015-classes”] as shown below:
{
"plugins": [
// "syntax-dynamic-import",
"transform-object-rest-spread",
],
"compact": false,
"presets": [
["env", {
// Try to take advantage of Webpack's scope-hoisting by not
// transpiling modules in Babel.
"modules": false,
// This is needed so that I don't run into an issue on the client
// with trying to create an already-transpiled class like
// RestClient.
"exclude": ["transform-es2015-classes"],
// Use polyfills for something like Promise or Map for browsers like
// IE. Note that this is an older version of babel-preset-env
// (1.6.1), so "true" is indeed the correct argument rather than
// "usage" or "entry", and core-js needs to be installed explicitly
// for it to work due to using NPM2.
"useBuiltIns": true,
"targets": {
"browsers": ["last 2 versions", "safari >= 7", "ie > 8"],
},
}],
"react",
],
"env": {
"test": {
"presets": [
["env", {
"exclude": ["transform-es2015-classes"],
}],
"react"
]
}
}
}
  • Note that I am not on Babel 7, so apparently I can’t use browserslistrc and thus need to manually list out the browsers that I’m targeting.

  • Because I didn’t have a babelrc already but I did have a webpack.config.js, I needed to remove my babel configuration from webpack.

  • I needed a jest.config.js for ignoring LESS files (see below)

  • If you’re migrating from another framework like Mocha, Chai, etc., you can follow this guide

  • Configure .eslintrc.json

    • Add “jest” to “plugins”
    • Add “plugin:jest/recommended” to “extends”
  • Modified my client/package.json to add some scripts (reference)

    • “test”: “jest”
    • “test:coverage”: “jest —coverage && start ./coverage/lcov-report/index.html”,
    • “test:watch”: “jest —watch”,
    • “test:debug”: “node —inspect-brk ./node_modules/.bin/jest —runInBand —watch”,
    • “test:debug:win”: “node —inspect-brk ./node_modules/jest/bin/jest.js —runInBand —watch”
    • Note: the test:debug:win version actually works on all platforms.
  • Consider installing jest-watch-typeahead (reference)

    • npm install —save-dev jest-watch-typeahead
    • If you’re going to install this, you also need to modify your jest.config.js (reference)
    • This also involves being on at least Jest v23, so you may need to do “npm install —save-dev jest@latest”.
    • Once done, you can run “jest —watch”, then press enter to cut off any future tests, and finally follow the filter help that shows (generally just press “P” and start typing a file name).

HiDeoo wrote up a nice starting guide for me (in the reference link above).

Typically, you want to write your unit tests before you move on to anything scenario- or integration-related. When you DO get to that point, if you have thunks dispatching other thunks, you can use FlushThunks from redux-testkit so that you can wait for everything to finish before checking the state.

jest.config.js and “extending” a parent config

Section titled jest.config.js and “extending” a parent config

The file is just a JavaScript file, so you can do something like this:

const baseConfig = require('../../jest.config.js');
module.exports = {
...baseConfig,
// Override anything here that you want to specify
};

THIS IS NOT A FUNCTION. It is just “.rejects.toThrow()” or “toThrowErrorMatchingSnapshot”:

await expect(
dbFramework.sellItem(userId, cosmeticItemIdYouDontOwn, sellAmount)
).rejects.toThrow();

So if you have a function that you would have called with “await someFunction”, you do what you see above:

await expect(someFunction).rejects.toThrowErrorMatchingSnapshot();

For synchronous code, the “expect” call takes in a function, otherwise it wouldn’t be able to work because it would throw immediately:

expect(() => functionWithAnError()).toThrow();

Verifying an error was thrown with a custom property

Section titled Verifying an error was thrown with a custom property

I sometimes have cases where I throw an Error from my development code that has a special property like “serverCode” or “clientCode”. From my test, I want to do something like this:

try {
callFunction();
throw new Error('should not make it here');
} catch(error) {
expect(error.code).toBe(code);
}

First, to explain the reasoning behind that code - we expect the function in the ‘try’ block to fail, so if it succeeds and we don’t have the “throw new Error”, then the test will pass unexpectedly.

Instead of “throw new Error”, the only real improvement that I could use here until Jest changes “toThrow” is to write this line:

expect(true).toBe(false);

Suppose you have a function that you know should fail and you care about its error message:

await expect(
dbFramework.testSetLeaguesForSeason(leagueInfo)
).rejects.toMatchSnapshot();

You can do this with toThrowErrorMatchingSnapshot as well (I prefer this way):

await expect(
dbFramework.testSetLeaguesForSeason(leagueInfo)
).rejects.toThrowErrorMatchingSnapshot();

You should not need expect.assertions unless you’re using callbacks, but if you’re using callbacks, then you have more to fix than just adding “expect.assertions”.

If you ever want extra matchers like “toBeArray”, you can look at this package.

See this note.

Because I’m on Windows and I have an old version of Node, here’s the command I have to run:

node —inspect-brk ./node_modules/jest/bin/jest.js —runInBand

If this doesn’t work for you, then it could require an update to at least Node v8.4 (see issue #1652)

TIMEOUT ISSUE: keep in mind that while debugging, the Jest default timeout per test of 5000 ms (reference) could be hit, and it may not be obvious that it’s happening in that case. To adjust the timeout of a test, the signature is “test(name, fn, timeout)”, so just put 1e9 as the timeout.

**Make sure not to check in the changed timeout! **

WATCH ISSUE (possibly fixed, check this): apparently you can’t use “—watch” when you’re debugging, so either specify the exact file that you want so that you don’t have to stop execution and filter down to it, or allow all files to run. To specify a single file, just do something like this:

node —inspect-brk ./node_modules/jest/bin/jest.js —runInBand ./test/react/components/hardwareloadout.test.js

DO NOT USE BACKSLASHES FOR THE TEST PATH UNLESS YOU DOUBLE THEM AS SHOWN BELOW:

.\test\react\components\hardwareloadout.test.js

I highly suggest reading on to “Sourcemaps” to find out how to get sourcemaps working, otherwise debugging could be very tough.

Turns out you don’t need to specify sourceMaps yourself in your babelrc because babel-jest will inline them for you (but if you did have to, here’s what it would look like):

{

“env”: {

“test”: {

“presets”: [

[“env”, {

“exclude”: [“transform-es2015-classes”],

}],

],

“sourceMaps”: “inline”

}

}

}

However, this can lead to strange issues debugging. For example, I had ES6 (which I wrote) that looked like this:

initialize() {

return this.setupDatabase().then(() => {

return this.databaseWrapper.connect();

});

}

However, the ES5 that it transpiled really turned “this” into “_this4”, so it was nearly impossible for me to actually debug using the console (due to this Chromium issue). Ways around this behavior:

  1. Turn off sourcemaps in Chrome (and refresh the page) so that you’re looking at transpiled code (reference)
    1. Note: when code has a sourcemap, you’ll see [sm] in the tab:
  2. For this specific problem, arrow functions shouldn’t even be transpiled since they’re supported natively by Node, so this was a configuration problem. I needed to make sure babel-preset-env knew to target the current version of Node:

“env”: {

“test”: {

“presets”: [

[“env”, {

“exclude”: [“transform-es2015-classes”],

targets: {

node: ‘current’,

},

}],

],

“sourceMaps”: false,

}

}

From Lumie1337: @Adam13531 regarding the sourcemaps issue, apparently it is an open issue for chrome, tldr: mapping from source code without source maps to source mapped symbols works, but not the other way around https://bugs.chromium.org/p/chromium/issues/detail?id=327092

Running common code between different tests

Section titled Running common code between different tests

Simple example: suppose you always want to import SomeModule from every test. You can make use of setupTestFrameworkScriptFile:

  1. In jest.config.json, add

setupTestFrameworkScriptFile: ‘<rootDir>/test/setupjest.js’,

  1. Make a setupjest.js that just has “import SomeModule from whatever”;

Note that setupTestFrameworkScriptFile is very similar to setupFiles, so if you’re ever using them both, you may want to be careful about naming.

I was testing Joi and noticed that it was emitting “then” and “catch” properties everywhere even though they weren’t relevant to my tests. Originally, I just called “delete” on both of those, but then HiDeoo showed me the reference link, and now I can do something like this:

14:28 HiDeoo: Ho nvm Adam13531, I remembered it wrong, it’s not yet doable as-is in an expect.extend, we use a fork to have toMatchSnapshotAndIgnoreKeys() but they don’t expose the original toMatchSnapshot() in expect.extend so we had to fork it to expose it. There is a PR coming up for this to expose it but not yet merged.

Note: if you’re going to go this route, the location where you’d add the code would be based on setupTestFrameworkScriptFile in your jest.config.js (reference). I wrote a config like this:

module.exports = {

setupTestFrameworkScriptFile: ‘<rootDir>/test/setuptestframework.js’,

};

All I had to do is put this in my package.json file under “scripts”:

“test:coverage”: “jest —coverage”

Then I did “npm run test:coverage” and got a coverage/lcov-report/index.html that I could open.

All you have to do is make a jest.config.js that looks like this:

module.exports = {

collectCoverageFrom: [‘<rootDir>/whatever/**/*.js’],

};

Note: “<rootDir>” is actually a token for Jest indicating the root directory.

You technically can cover files in node_modules even though Istanbul ignores them by default (and Jest uses a fork of Istanbul). Check out this issue for help with setting it up. Here’s a public example of that in action.

When running a test with “toMatchSnapshot” for the first time, a snapshots directory is going to get created alongside the test. This will contain some JavaScript objects/strings that represent what was returned by your test.

Subsequently, it will compare the output of the test against whatever’s in the snapshot files. Suppose you run a test and it reports a mismatch against the snapshot (meaning the test has errors), but you don’t want them to be considered as errors, you can press ‘u’ in “jest —watch” to update the snapshot files.

You should check in your snapshots folders.

If you expect your test to fail, then instead of saying something like:

expect(passingFunction()).toMatchSnapshot()

say

expect(() => failingFunction()).toThrowErrorMatchingSnapshot();

To get snapshots directly into your test code without needing a new file, take a look at this:

[13:53] HiDeoo: Btw adam13531 I don’t know what Jest version you’re using, but in 23.3 they introduced inline snapshots with toMatchInlineSnapshot() & toThrowErrorMatchingInlineSnapshot() and it’s amazing, no more snapshot files & you can see the snapshot right from the test https://bit.ly/2mFmqs6

If you have a generated user ID or date, you may only want to check that the type is correct. This is where property matchers come into play. However, there may be a time when you want to ignore a property altogether (e.g. a database property that could be null or a date); in those cases, you’re not really testing anything (since the database will enforce that constraint for you just by virtue of columns having types), so just omit the property from the test:

const dbSeasonLeaguesJestMatcher = {
league_id: expect.any(Number),
season_id: expect.any(Number),
};
_.map(dbSeasonLeagues, (dbSeasonLeague) => {
delete dbSeasonLeague.created_at; // this could be null or a date, so just delete it so that it doesn't end up in the snapshot
expect(dbSeasonLeague).toMatchSnapshot(dbSeasonLeaguesJestMatcher);
});

Here’s another example:

// Example output from the test
{
"device_id": "made up",
"user_id": 46,
"created_at": "2019-03-20T18:08:10.000Z",
"updated_at": "2019-03-20T18:08:10.000Z",
"token": "made up token",
"aws_sns_arn": "some ARN",
"platform": "ANDROID"
}
// Matcher used to match that object
const matcher = {
user_id: expect.any(Number),
updated_at: expect.any(Date),
created_at: expect.any(Date),
};
expect(dbResp).toMatchSnapshot(matcher);

Just follow the instructions at the reference; it’s an installation and a modification to jest.config.json. The way that it works is it will return the name used to index your CSS modules. E.g. if, in production, you specify “styles.button” and you end up getting “_1VRY1S3Kahz8E2HlNBR-U5”, then in your test, it would literally show as the string “button” (since that’s the key that you used).

There are a couple of reasons why you might want to handle LESS files instead of just ignoring them:

  1. Your snapshots of DOM elements will make more sense rather than having “class=undefined” (or class="") everywhere
  2. You may want to fetch an element by its class, in which case having the identifier without having to modify dev code is desirable.

Just FYI: you can “properly” handle LESS files too (reference). If you go that route, then you won’t get “class=undefined” or ‘class=""’ for all of your DOM snapshots.

The reference clearly describes two ways. If you want to go the config route, put it into jest.config.js:

module.exports = {
"moduleNameMapper": {
".*\.less$": "<rootDir>/pathToDummyFile/dummy.js"
}
};

Note that <rootDir> is not just for the sake of example. Also, you’ll need to restart Jest after doing this.

I am pretty sure I ran into this issue. It was frustrating enough where I just explicitly put my babelrc back into Webpack for the sake of deploys and kept a separate .babelrc file just for sake of Jest.

If I ever need to take another look at this, I could specify “debug: true” in babelrc so that I can figure out which plug-ins are being used.

There’s a bug in console.time at the time of writing (6/12/2018) at these two lines of code: they divide by 1000 but then show the resulting time in ms. This means that if you use console.time and your times seem off by a factor 1000 that it’s not you. ;)

UPDATE (6/20/2018): this has been fixed

Random failure that you can’t chalk up to anything else

Section titled Random failure that you can’t chalk up to anything else

It’s possible that you forgot to reset mocks. If running the test in isolation works, then this is a stronger possibility since it means you didn’t clean up after a previous test.

I was trying to compare dates that I’d received from knex, but it wasn’t working. This code:

expect(accountCreationDate).toBe(dbUser.creation_date);

…was producing this result:

Expected: 2018-07-31T21:58:59.000Z

Received: 2018-07-31T21:58:59.000Z

I should have been using “toEqual” instead of “toBe” and it would have worked (reference).