Skip to content

How To: Write a unit test in javascript

mmbutton edited this page Aug 18, 2020 · 1 revision

Tool Documentation

Jest: https://jestjs.io/ Enzyme: https://github.com/airbnb/enzyme

How to Test non-components

Basic javascript testing only requires a knowledge of Jest.

  1. Create a file with the same name and location as your component, but with a test suffix before the .js suffix (ex: MyComponent.test.js)
  2. Import everything that you will be testing
  3. Optional: Use globals to do stuff before tests are run. You can see what each global does here: https://jestjs.io/docs/en/api 4.Create a test suite
describe('This is my test suite', () => {
  // All tests for this suite go into this area
});

5.Optional: Auto generate mocks. You can find Jests mock API here: https://jestjs.io/docs/en/mock-function-api 6. Create a test and use expect() for test validation. You can find the API of expect here: https://jestjs.io/docs/en/expect

it('Does stuff to foo', () => {
  const mockFoo = {bar: 'Mocked bar'};

  let expectedReturn = {bar: 'Bar has been changed'}
  let actualReturn = MyImport.doesStuffToFoo(mockFoo);
    expect(actualReturn).toEqual(expectedReturn);
  });
  1. Add more tests until the test suite is complete, then add more test suites until thoroughly tested.

How To Test a Component

  1. Create a file with the same name and location as your component, but with a test suffix before the .js suffix (ex: MyComponent.test.jsx)
  2. Add the above code to set up imports
import React from 'react';
import ReactDOM from 'react-dom';
import { shallow, mount, render, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import <MyComponent> from './<MyComponent>';


configure({ adapter: new Adapter() });
  1. Add a test suite for each component being tested
describe('<MyComponent />', () => {
  // All tests for <MyComponent /> go here
});
  1. Render your component within each test. Mount does full DOM rendering. Use it for testing lifecycle methods in hooks and integration with DOM API's. Documentation: https://airbnb.io/enzyme/docs/api/mount.html
it('calls componentDidMount', () => {
  sinon.spy(Foo.prototype, 'componentDidMount');
  const wrapper = mount(<Foo />);
  expect(Foo.prototype.componentDidMount).to.have.property('callCount', 1);
});

Material UI Testing

The material-ui library contains utilities to support testing, see https://material-ui.com/guides/testing/. Because most components we develop using material-ui are going to wrap the component with the WithStyles HOC then shallow doesn't create a wrapper with the component. Enzyme supports dive() which goes down one more level into the component hierarchy. To simplify this the createShallow() material-ui util takes a prop "{ dive: true }" which wraps the result of calling dive() on the test component. createMount can also be used.
import React from 'react';
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import MaterialButton from '@material-ui/core/Button';
import { createShallow } from '@material-ui/core/test-utils';

import Button from '../Button';

configure({ adapter: new Adapter() });

describe('<Button />', () => {
  let shallow;

  beforeAll(() => {
    shallow = createShallow({ dive: true });
  });

  it('contains a material button', () => {
    const wrapper = shallow(<Button buttonType="save" />);
    expect(wrapper.find(MaterialButton).length).toBe(1);
  });
});

How To Test Asynchronous Code

Case 1: Promises

There are 3 ways to handle promises for testing in jest. None are particularly better than the others and they can be mixed and matched. I'd suggest using the first 1 as it allows testing multiple promises within the same test.

The below block is the code being tested in each case.

const someService = {
  getStuff: async () => {
    try {
	  const response = await WebRequest.get('SomeService', {
		parameterOne: 1
      });
	  const payload = WebRequest.parseDmasAPIResponse(response);
      return payload;
    } catch(error) {
      throw error;
    }
  }
}
export default someService;

Using async & await

import React from 'react';
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import someService from '../someService';
jest.mock('util/WebRequest');

configure({ adapter: new Adapter() });

const response = {
  data: {
    statusCode: 0,
    payload: {
      stuff: '1',
    },
  },
};
const errorMessage = new Error('Palantir 404 error');

describe('someService', () => {
  beforeAll(() => {
    jest
      .spyOn(WebRequest, 'parseDmasAPIResponse')
      .mockImplementation(() => response.data.payload);
  });

  it('Performs a get successfully', async () => {
    jest
      .spyOn(WebRequest, 'get')
      .mockImplementation(() => Promise.resolve(response));

    await expect(someService.getStuff()).resolves.toBe(
      response.data.payload
    );
  });

  it('Catches errors on get', async () => {
    jest
      .spyOn(WebRequest, 'get')
      .mockImplementation((url, params) => Promise.reject(errorMessage));

    await expect(
      someService.getStuff()
    ).rejects.toBe(errorMessage);
  });
});

Using .then and .catch

import React from 'react';
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import someService from '../SomeService';
jest.mock('util/WebRequest');

configure({ adapter: new Adapter() });

const response = {
  data: {
    statusCode: 0,
    payload: {
      stuff: '1',
    },
  },
};
const errorMessage = new Error('Palantir 404 error');

describe('someService', () => {
  beforeAll(() => {
    jest
      .spyOn(WebRequest, 'parseDmasAPIResponse')
      .mockImplementation(() => response.data.payload);
  });

  it('Performs a get successfully', async () => {
    jest
      .spyOn(WebRequest, 'get')
      .mockImplementation(() => Promise.resolve(response));

    return someService.getStuff().then(payload => {
      expect(payload.stuff).toBe(1)
    })
    .catch(error => {
      throw error;
    }
  });

  it('Catches errors on get', async () => {
    jest
      .spyOn(WebRequest, 'get')
      .mockImplementation((url, params) => Promise.reject(errorMessage));

    return someService
      .getStuff()
      .catch(error => {
   	expect(error).toMatch(errorMessage);
      }
  });
});

case 2: Asynchronous code called in a synchronus function (or call triggered by lifecycle methods)

This is common with hooks useEffect function and setting the state not updating correctly.

class SomeClass extends Component {
  ComponentDidMount() {
    someService.getStuff().then(payload => {
	  // Do something with payload
	})
	.catch(error) {
	  throw error;
    }
  }

  render() {
    return <div />;
   }
}

The above code doesn't return a promise and is not asynchronous so the previous methods won't work. In our test code we can use flushPromises() to force the Promise to resolve before our next step.

// enzyme and class imports
import flushPromises from 'flush-promises';

it('some service call does something', async () => {
    jest
      .spyOn(someService, 'getStuff')
      .mockImplementation(jest.fn(() => Promise.resolve({stuff: 1})));
    const wrapper = mount(<SomeClass />);

    // wait for next async event to complete.
    await flushPromises();
    expect(someService.getStuff).toHaveBeenCalled();
  });