Testing Javascript isn’t always straightforward but is always important. In this article we will learn many different ways to test different aspects of Javascript code. Some of the topics that will be covered include:
- Testing authentication APIs
- Linting with ESLint
- Using WebdriverIO for functional browser testing
- Using Jest for unit testing and mocking
- Jest snapshot testing
- And much more…
Let’s go 👇👇🚀
Testing a login API
Component tests with JavaScript
What are component tests?
Component tests are like integration tests that validate that your front-end UI component is working as expected. We don’t need to render the entire page and go through a whole flow to test a component. Instead, we can test some specific piece in isolation.
Component tests are an excellent balance between tests that are too big and tests that are too small.
Code Coverage
Code coverage with react testing library
"scripts": { "test:coverage": "react-scripts test --coverage --watchAll=false" },
Thanks to SO for the recommendation
You can then find the coverage report by running open coverage/lcov-report/index.html
Configuring Jest test coverage
Be sure to specify where we want to collect the coverage from by setting up a jest.config.js
that looks like this
//jest.config.js module.exports = { testEnvironment: 'jest-environment-jsdom', moduleDirectories: [ 'node_modules', path.join(__dirname, 'src'), 'shared', path.join(__dirname, 'test'), ], setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], snapshotSerializers: ['jest-emotion'], // collect coverage from all the files under src that end in .js collectCoverageFrom: ['**/src/**/*.js'], }
Ensure high code coverage for the important files
//jest.config.js coverageThreshold: { global: {}, // We want this file to have really high code coverage './src/shared/utils.js': { statements: 100, branches: 80, functions: 100, lines: 100, }, }, projects: [ './test/jest.lint.js', './test/jest.client.js', './test/jest.server.js', './server', ], }
Jest in debug mode
Add debugger
somewhere in your code
Add a new script to package.json
"scripts": { //This command will run node and will wait for our Jest process. `--runInBand` Instead of running in parallel, it will run serially to make debugging work. "test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --watch"
Testing JavaScript with ESLint
ESLint is a static code analysis tool that makes sure that your code follows standard JavaScript standards. Code standards are important in both test code and production code.
Install Eslint
npm i eslint --save-dev
This will populate the package.json to look like this
// package.json
"devDependencies": {
"eslint": "^7.6.0"
}
We can now configure ESLint by running
$ npx eslint --init
and then navigating through the menu options to set the appropriate configuration. I like AirBnB
This will create a .eslintrc.js
in our repository
//.eslintrc.js
module.exports = {
env: {
browser: true,
commonjs: true,
es2021: true,
},
extends: [
'plugin:react/recommended',
'airbnb',
],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
},
plugins: [
'react',
],
rules: {
},
};
Run ESLint
npx eslint .
references this source code
Fixing linting errors automatically
Running the command below will make Eslint try to fix as many errors as it can
npx estlint . --fix
Here is the code before
const username = 'freddy'
typeof username === 'strng'
let undefined;
if (!('serviceWorker' in navigator)) {
// you have an old browser :-(
}
const greeting = 'hello'
console.log(`${greeting} world!`)
;[1, 2, 3].forEach(x => console.log(x))
This is the code after
const username = 'freddy';
typeof username === 'string';
if (!('serviceWorker' in navigator)) {
// you have an old browser :-(
}
const greeting = 'hello';
console.log(`${greeting} world!`);
[1, 2, 3].forEach((x) => console.log(x));
How to run Eslint in NPM
First, let’s add .eslintignore
.js file so that we are not linting the dist directory of our code
//.eslintignore.js
node_modules
dist
Also, we can add a script to our package.json
// we can now execute npm run lint
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint ."
},
It might be less redundant to avoid creating a new .eslintignore file and simply add that to the script
command of our package.json
like this
eslint --ignore-path .gitignore .
Testing JavaScript with Jest
Clone the repository here and follow setup instructions
app.js
starts our application and most of the logic lives in calculator.js
Open this branch
Install Jest npm i jest --save-dev
and it will now exist in the package.json
Now add this LOC to the package.json so that we can run Jest
"scripts": {
"test" : "jest"
},
npm test
will now execute Jest and will complain that we don’t have any tests exisiting in the __tests__ directory
Jest will automatically run any test in the __tests__ directory.
Useful Jest commands
To run a test by test name
npx jest -t 'should fetch users'
To set watch mode for Jest tests
npx test --watch
Setting jest.confg.js
module.exports = {
testEnvironment: 'jest-environment-jsdom',
}
We set the code in the config file so that it will ensure that all of our tests run in a browser simulated environment thanks to jsdom npm module.
Mocking with Jest
Corresponds with this code base.
module.exports = {
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
'\\.css$': require.resolve('./test/style-mock.js'),
},
}
The moduleNameMapper allows us to mock out the .css files for our tests. Basically it say’s that anything ending in .css will become the file at that path. The file at the path looks like the code below. Ultimately, this file is just exporting a blank module for the purposes of mocking.
module.exports = {}
If we didn’t have this and tried to run npm run test, we would get the following error because React is trying to process .css files as JS files.
SyntaxError: Unexpected token '.'
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
> 3 | import styles from './auto-scaling-text.module.css'
That comes from test file auto-scaling-text.js
when the test is executed.
import 'react-testing-library/cleanup-after-each'
import React from 'react'
import {render} from 'react-testing-library'
import AutoScalingText from '../auto-scaling-text'
test('renders', () => {
render(<AutoScalingText />)
})
Adding a console.log(styles)
in the auto-scaling-text.js
and running npm run test
will display the output of that file as an empty object.
How to see the output of JSDom?
If we wanted to see the HTML output of the <AutoScalingText /> component then we can write code that looks like this inside of our test:
test('renders', () => {
const {debug} = render(<AutoScalingText />)
debug()
})
If we run this test then we will get output in our console that looks like this
console.log node_modules/react-testing-library/dist/index.js:57
<body>
<div>
<div
data-test="total"
style="transform: scale(1,1);"
/>
</div>
</body>
How to use JS DOM snapshots?
Snapshots are a really excellent way to run and maintain tests that need to validate a lot of data.
Imagine we start with a test that looks like this
const {getFlyingSuperHeros} = require('../super-heros')
// A test that needs to statically validate the values in the array
test('returns returns super heros that can fly', () => {
const flyingHeros = getFlyingSuperHeros()
console.log(flyingHeros)
expect(flyingHeros).toEqual(
[ { name: 'Dynaguy', powers: [ 'disintegration ray', 'fly' ] } ])
})
That validates the content of this file
// super-heros.js
const superHeros = [
{name: 'Dynaguy', powers: ['disintegration ray', 'fly']},
{name: 'Dash', powers: ['speed']},
]
function getFlyingSuperHeros() {
return superHeros.filter(hero => {
return hero.powers.includes('fly')
})
}
module.exports = { getFlyingSuperHeros }
Imagine if we added more data to the superHeros object then we need to update our test to reflect the new data. This can obviously become pretty annoying.
We can use the toMatchSnapshot() instead that will automatically take a snapshot of the data and save it in a file super-heros-1.js.snap.
// Jest Snapshot v1, https://goo.gl/fbAQLP
//it's much more maintainable to use snapshots instead of copy + paste
test('returns returns super heros that can fly', () => {
const flyingHeros = getFlyingSuperHeros()
console.log(flyingHeros)
expect(flyingHeros).toMatchSnapshot()
})
//super-heros-1.js.snap
exports[`returns returns super heros that can fly 1`] = `
Array [
Object {
"name": "Dynaguy",
"powers": Array [
"disintegration ray",
"fly",
],
},
]
`;
To update the snapshot use
jest --updateSnapshot
Matching inline snapshots
The snapshot feature is really nice but we can make it even better. We can actually have the test data for the acceptance criteria to automagically appear in our test files. Allowing us to see the entire test in a single file using
toMatchInlineSnapshot()
Running this test for the first time
test("uses inline snapshot", () => { const flyingHeros = getFlyingSuperHeros(); console.log(flyingHeros); expect(flyingHeros).toMatchInlineSnapshot()
will update our test automatically pull in the Array object that we are trying to validate
test("uses inline snapshot", () => { const flyingHeros = getFlyingSuperHeros(); console.log(flyingHeros); expect(flyingHeros).toMatchInlineSnapshot(` Array [ Object { "name": "Dynaguy", "powers": Array [ "disintegration ray", "fly", ], }, ] `); });
We will need prettier installed as a –save-dev.
Useful Jest commands
npx jest –watch to run Jest in watch mode
Testing JavaScript with Cypress
Install Cypress with
npm i --save-dev cypress
More on ESLint
Configure the `parserOptions` so that it’s using the latest version of ecma script
{
"parserOptions": {
"ecmaVersion": 2019,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
}
}
Now let’s add some rules to our linting config
"rules": {
"valid-typeof": "error"
}
Adding the rule above and then running
npx eslint .
will throw the following error.
/Users/nikolay/Documents/source/js/js-code/linting/src/example.js 2:21 error Invalid typeof comparison value valid-typeof
Because we have a typo on line 2 in our example.js file
const username = 'freddy'
typeof username === 'strng'
We can add a few more rules that look like this
"rules": {
"valid-typeof": "error",
"no-unsafe-negation" : "error",
"no-unused-vars" : "error",
"no-unexpected-multiline" : "error",
"no-undef" : "error"
}
Then running npx eslint .
will show the following errors
Here is the file with the errors
const username = 'freddy'
typeof username === 'strng'
let undefined;
if (!('serviceWorker' in navigator)) {
// you have an old browser :-(
}
const greeting = 'hello'
console.log(`${greeting} world!`)
;[1, 2, 3].forEach(x => console.log(x))
And if we want to fix the ‘console’ is not defined error then we can add the following to our .eslintrc
"env": {
"browser": true
}
Tip: if you are using VSCode, you can use the Eslint extension
Pre-configured Eslint rules
If we want an easier way to configure our ESlint rules, we can use defaults like this
{
"parserOptions": {
"ecmaVersion": 2019,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"extends": ["eslint:recommended"],
"env": {
"browser": true
}
}
Then, rerunning npx eslint .
will still return 2 errors as before.
Below is an example of the file with a few more errors and how they are displayed by Eslint
const username = 'freddy'
typeof username === 'strng'
let undefined;
if (!('serviceWorker' in navigator)) {
// you have an old browser :-(
}
const greeting = 'hello'
console.log('${greeting} world!')
[1, 2, 3].forEach(x => console.log(x))
Webdriver IO for JavaScript Testing
WebdriverIO is a front-end functional testing library.
How to debug JS in Visual Studio Code?
https://johnpapa.net/debugging-with-visual-studio-code/
Comparing testing frameworks
Framework Name | Github Stars | Features | Disadvantages |
Jest | 22,000 | React, spies, mocks, snapshot testing, code coverage, great for beginners | |
Mocha | 16,000 | Used with 3rd party assertions, mocking, spying tools. Flexible and open to extension. Lots of resources | Runs only serially? |
Jasmine | 14,000 | Official testing framework for Angular | Seems to be losing support |
Karma | 10,000 | Run tests in the browser or browser-like environment with jsdom. Support for CI tools like Travis and Jenkins. Write your tests using Jasmine or Mocha. Tests can run remotely through a service like BrowserStack | |
Puppeteer | 43,000 | Built by Google, Node library, for Chrome or Chromium, headless or non-headless, fast |
Framework | Technologies | Command to run tests |
https://github.com/wswebcreation/demo-ts-protractor | TypeScript, Cucumber, Jasmine | |
https://github.com/saucelabs-sample-test-frameworks/JS-Protractor-Selenium | JS, Protractor | |
https://github.com/christian-bromann/webdriverio-performance-testing | WebdriverIO, JS |
|
https://github.com/saucelabs/extended-debugging-JS-examples | WebDriver.IO, Protractor, Jasmine, WebDriverJS Mocha, Nightwatch |
Great post about JavaScript frameworks
NPM Stuff
This command npm install --save-dev jest
installs the package and places the dependency into `package.json` which is a file that tells us about all of the dependencies.
Update NPM packages to latest version
BDD Resources
Excellent resources
- Brian Mann on testing the UI, login, and atomic tests with Cypress
- JavaScript Testing Best Practices by Yoni Goldberg
Hands-on workshops
- Component tests Workshop
- Testing Javascript with Kent C. Dodds ($332)
Blog posts
- How to test Angular Apps blog post
- Using Webdriverio for UI automation
- A blog post from Allister Scott summarizing a bunch of different technologies and advantages and disadvantages