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

example code coverage report

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

Errors produced based on our source code
Errors produced based on our 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 video training

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

Errors produced based on our source code
Errors produced based on our source code

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.

Read this in-depth article

How to debug JS in Visual Studio Code?

https://johnpapa.net/debugging-with-visual-studio-code/

Comparing testing frameworks

Framework NameGithub StarsFeaturesDisadvantages
Jest22,000React, spies, mocks, snapshot testing, code coverage, great for beginners
Mocha16,000Used with 3rd party assertions, mocking, spying tools. Flexible and open to extension. Lots of resourcesRuns only serially?
Jasmine14,000Official testing framework for AngularSeems to be losing support
Karma10,000Run 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

Puppeteer43,000Built by Google, Node library, for Chrome or Chromium, headless or non-headless, fast
FrameworkTechnologiesCommand to run tests
https://github.com/wswebcreation/demo-ts-protractorTypeScript, Cucumber, Jasmine
https://github.com/saucelabs-sample-test-frameworks/JS-Protractor-SeleniumJS, Protractor
https://github.com/christian-bromann/webdriverio-performance-testingWebdriverIO, JS"test:sauce": "npm run wdio wdio.sauce.conf.js"
https://github.com/saucelabs/extended-debugging-JS-examplesWebDriver.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

Use this documentation

BDD Resources

Excellent resources

Hands-on workshops

Blog posts