Frontend Scrapbook

Notes that make a difference

Testing ReactJS Applications

By admin

on Mon Sep 14 2020

In tests, we Arrange, Act, and Assert!

Configuring Jest in react applications

npm install --save-dev jest

In package.json,

"scripts" : {
  ....
  ....
  "babel": {
    "presets": "./.babelrc.js"
  },
  "test":"jest"
}

Jest picks up what's in babelrc and apples to all tests.

In .babelrc.js
const isTest = String(process.env.NODE_ENV) === 'test'

module.exports = {
  presets: [['env', {modules: isTest ? 'commonjs' : false}], 'react'],
  plugins: [
    'syntax-dynamic-import',
    'transform-class-properties',
    'transform-object-rest-spread',
  ],
}

// presets should be set to use modules as false when webpack is used. This asks babel not to transpile modules ( import/export ) Webpack has support for modules and is useful for tree shaking.

By default Jest actually loads JSDOM. ( so, we have access to window object in tests ) – We can add configuration for Jest if we don’t require JSDOM ( access to the window ). This can be done by adding ‘jest-environment-node’ / ‘node’ to testEnvironment key in package.json

"jest": {
 "testEnvironment":"jest-environment-jsdom" // default value
}

When importing CSS through import statements, node can’t import CSS. It will treat anything you import as either JSON or JS module. To fix this, we need to create a style-mock js file that act as a mapper. Create style-mock.js in a folder named test in root of the project. We could also have jest.confg.js . Jest will automatically pick the config

module.exports = {
 testEnvironment: 'jsdom', //default
 moduleNameMapper: {
    '\\.css$': require.resolve('./test/style-mock'),
  }
}

//style-mock.js
module.exports = {}

When using CreateReactApp, *.module.css will treat as CSS modules. It means that we can have a .css file and import and have access to all CSS properties as key-value pairs.

//styles.module.css
.auto-scaling-text {

}
//in JS
import styles from 'styles.module.css';
styles.autoScalingText // accessor

If style-mock.js export an empty object, we won’t get the classes which we import in our tests.

test('mounts', () => {
  const div = document.createElement('div')
  ReactDOM.render(<AutoScalingText />, div)
  console.log(div.innerHTML) //renders <div></div> instead of <div class="autoScalingText"></div>
})

For all files ending with .module.css, we could configure jest to use identity-obj-proxy npm module so that we div.innerHTML is access we will see the className that was used in JS file.

npm install --save-dev identity-obj-proxy

so we had something like this in component,

return (
      <div
        className={styles.autoScalingText}
        style={{transform: `scale(${scale},${scale})`}}
        ref={node => (this.node = node)}
      >
        {this.props.children}
      </div>
    )
}

It will output class as whatever we refer after 'styles.' in the rendered HTML

...
...
  
//jest.config.js
module.exports = {
  moduleNameMapper: {
    '\\.module\\.css$': 'identity-obj-proxy', // npm module
    '\\.css$': require.resolve('./test/style-mock'),
  },
}

Adding code coverage, we can add in package.json

scripts: {
 "test": "jest --coverage"
}

In Jest configuration, we could add thresholds for test coverage. Below means that overall each section should coverage the percentage mentioned. Else, tests fail with a message like below

Jest: “global” coverage threshold for functions (22%) not met: 21.74%

//jest.config.js
module.exports = {
 moduleNameMapper: {
    '\\.module\\.css$': 'identity-obj-proxy', // npm module
    '\\.css$': require.resolve('./test/style-mock'),
 },
 coverageThreshold: {
    global: {
      statements: 18, // percentages
      branches: 10,
      functions: 22,
      lines: 18,
    },
 }
}

Mocks

Integrating with third-party services, we could moke services/utils in tests

We could return the mock module/object in the callback.

//consider we are importing '../utils/api' in the COMPONENT file. Since tests live in __tests__, mock with jest with relative path to this the module.

import * as utilsMock from '../../utils/api';
jest.mock('../../utils/api', () => {
  return {
    posts: {
      create: jest.fn(() => Promise.resolve()),
    },
  }
});

utilsMock will have our implementation.

We could use async functions as callbacks in tests and use a function like flushPromises which will resolve in the next tick of event loop and have our assertions.

const flushPromises = () => {
  return new Promise((res) => {
    setTimeout(res, 0)
  })
}

test('calls onSubmit with username and password',async function() {
 ....
 ReactDOM.render(<Editor />, container);
 const submit = new wndow.Event('submit');
 const form = contianer.querySelector('form');
 form.dispatchEvent(submit)
 await flushPromises();
 expect(fakeHistory.push).toHaveBeenCalledTimes(1);
 expect(fakeHistory.push).toHaveBeenCalledWith('/');
 expect(utilsMock.posts.create).toHaveBeenCalledWith({
    user: fakeUser.id
    ....
    date: expect.any(String)
 });
 ....
});

Enzyme

It’s a tool to test React components. Our tests should resemble the way the application is used by the user. It gives more confidence to ship the code. In Enzyme, for example, has the concept of shallow render which renders only the outer components and not the components that this component renders ( composite components ). Even if we use Enzyme, it is always better to avoid Shallow rendering. Enzyme tests code based on implementation details than behavior, which is bad.

// using helpful utilities
import React from 'react'
import ReactDOM from 'react-dom'
// you'll need these:
import {generate} from 'til-client-test-utils'
import {render, Simulate, cleanup} from 'react-testing-library'
import Login from '../login'

afterEach(cleanup)
test('calls onSubmit with the username and password when submitted', () => {
  // Arrange
  // use generate.loginForm() here
  const fakeUser = generate.loginForm() //{username: 'test', password: '343g3q7243@#'}
  const handleSubmit = jest.fn()
  // use: render(<Login onSubmit={handleSubmit} />)
  // It'll give you back an object with
  // `getByLabelText` and `getByText` functions
  // so you don't need a div anymore!
  const {container, getByLabelText, getByText, unmount} = render(
    <Login onSubmit={handleSubmit} />,
    //{containr: div},
  )

  const usernameNode = getByLabelText('Username') //inputs[0]
  const passwordNode = getByLabelText('Password') //inputs[1]
  const formNode = container.querySelector('form')
  const submitButtonNode = getByText('Submit')

  usernameNode.value = fakeUser.username
  passwordNode.value = fakeUser.password

  // Act
  Simulate.submit(formNode) 
  //const event = new window.Event('submit')
  //formNode.dispatchEvent(event)

  // Assert
  expect(handleSubmit).toHaveBeenCalledTimes(1)
  expect(handleSubmit).toHaveBeenCalledWith(fakeUser)
  expect(submitButtonNode.type).toBe('submit');

  // unmount
  unmount();
})

test('', () => {
 console.log(document.body.innerHTML) //empty as we use cleanup
});

// Due to the fact that our element is not in the document, the
// click event on the submit button will not be treated as a
// submit event on the form which is why we're simulating a submit
// event on the form rather than clicking the button and then
// asserting the button's type is set to submit rather than just
// clicking on the button.
//
// Alternatively, we could actually insert the element directly into
// the document, then click on the button and that should work!
// (Tip: document.body.appendChild(container) / renderIntoDocument instead of render from react-testing-library and getByText('submit').click())

If we want to fire an actual event, we could 
import {fireEvent} from 'react-testing-library';
fireEvent.click(submitButtonNode)

We should test the DOM instead of testing the internal state of a component. This is because it is more closer to the user and we don’t test the implementation details but from the user perspective.

Snapshot Testing

Instead of manually changing the tests each time when data/return value changes, we could use snapshots. We should commit snapshots to the repo.

In a manual snapshot, we would console.log(flyingHeros) and copy the serialized version and manually updates the test. Instead, we could use toMatchSnapshot().

test.only('automatic snapshot', () => {
  const flyingHeros = getFlyingSuperHeros()
  //expect(flyingHeros).toEqual([
  //  {name: 'Dynaguy', powers: ['disintegration ray', 'fly']},
  //  {name: 'Apogee', powers: ['gravity control', 'fly']},
  //  {name: 'Jack-Jack', powers: ['shapeshifting', 'fly']},
  //])
  expect(flyingHeros).toMatchSnapshot()
})

We could use snapshot serializers which are used to inject custom data into the snapshots generated by Jest. For example, we could insert generated CSS (css-in-js) in generated snapshots apart from HTML.

//jest.config.js

module.exports = {
....
....
snapshotSerializers: ['jest-glamor-react'],
}

We could take snapshots of components as well.

test('snapshot', () => {
  const {container} = render(<Login />)
  expect(container.firstChild).toMatchSnapshot()
})