Skip to content

Instantly share code, notes, and snippets.

@axyz
Last active August 22, 2019 03:56
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save axyz/64c5087565b2c1907c0a8c4952cba27f to your computer and use it in GitHub Desktop.
Save axyz/64c5087565b2c1907c0a8c4952cba27f to your computer and use it in GitHub Desktop.

What to use

  • Test runner: ava
  • React components testing: enzyme
  • Endpoint testing: express + supertest
  • Mocking framework: sinon
  • External dependencies mocking: proxyquire

Test cases

all the following examples are using ava syntax, but they may be easily adapted to mocha or tape as well

react components

import test from 'ava';
import React from 'react';
import {shallow} from 'enzyme';
import Component from '../component';
// avoid using beforeEach
// all mocks should be provided by a factory function with consistent naming
function getComponentMock(opts) {
 const defaultOpts = {
   // props should be a separate property inside options, otherwise it
   // becomes hard to differentiate them from other options
   props: {
     bar: 2
   },
   children: ['foo']
 }
 const options = {...defaultOpts, ...opts);

 // shallow render should be preferred
 return shallow(
   <Component {...options.props}>
     {options.children}
   </Component>
 );
}

test('component should have bar set to 1', t => {
 const component = getComponentMock({
   props: {
     bar: 1
   }
 });

 t.is(component.prop('bar'), 1);
});

test('clicks should work', t => {
 // needed spies are created per test
 const onButtonClick = sinon.spy();
 const component = getComponentMock({
   props: {
     onButtonClick
   }
 });

 component.find('button').simulate('click');
 t.true(onButtonClick.calledOnce);
});

DOM interactions

import test from 'ava';
import sinon from 'sinon';
import React from 'react';
import { mount } from 'enzyme';
import { jsdom } from 'jsdom';
import Component from '../component';

function getComponentMock(opts = {}) {
 const defaultOpts = {
   // pass fakeDom as an option allow to use resulting dom state
   // from other components operations or multiple components rendering
   fakeDom: jsdom('<body></body>'),
   props: { bar: 1 },
 };
 const options = {...defaultOpts, ...opts};
 global.document = options.fakeDom;
 global.window = document.defaultView;
 global.navigator = window.navigator;

 return {
   component: mount(<Component {...options.props} />)
 };
}

test('some global is defined', t => {
 const {component} = getComponentMock();
 //assuming the component write the global when mounted
 t.is(window.__MY_DIRTY_GLOBAL__, 'EXPECTED');
});

test('multiple components', t => {
 const {component : component1} = getComponentMock();
 const {component : component2} = getComponentMock({
   // using the DOM state resulting from component1 rendering
   fakeDom: window.document
 });
 
 t.is(window.__MY_DIRTY_GLOBAL__, 'EXPECTED');
});

mock services

import test from 'ava';
import express from 'express';
import request from 'supertest';
// sample data should be in sync with official service API, but only contains keys 
// that we need, this way it would be easier to have a reference to stuff that must 
// remain retrocompatible on those services
import serviceGetRouteSample from 'serviceGetRouteSample.json';

function getServiceMock(opts) {
 const defaultOpts = {
   handlers: {
     getRoute: (req, res) => {res.json(serviceGetRouteSample)}
   }
 }
 const options = {...defaultOpts, ...opts);

 // we always create a brand new server instance to allow parallel testing
 const api = express();
 api.get('/route', opts.handlers.getRoute);
 api.post('/route', opts.handlers.postRoute);
 return api
}

test('service:Success', async t => {
 t.plan(2);
 //spies may be passed as handlers
 const service = getServiceMock();

 const res = await request(service)
   .post('/route')
   .send({foo: 'bar'});

 t.is(res.status, 200);
 t.is(res.body.email, 'bar');
});

getting props from API

import test from 'ava';
import React from 'react';
import {shallow} from 'enzyme';
import request from 'supertest';

test('fetching data for component', async t => {
 const service = getServiceMock();
 // we could directly pass some mocked json here, but I think it would be
 // better to have a single implementation for any mocked service and having
 // it to behave as a “real” server especially if it is going to be used
 // more than once
 const res = await request(service)
   .get('route');

 const component = getComponentMock({
   props: {
     data: res.body
   }
 });

 t.is(component.prop('data'), expectedData);
});

Mock Dependencies

import test from 'ava';
import proxyquire from 'proxyquire';
// ensure we don't get any module from the cache, but to load it fresh every time
proxyquire.noPreserveCache();
 
// never import the module to be tested globally, always use a factory function
// to get the proxyquire version of it
function getModuleMock() {
  const dep1 = sinon.stub();
  const dep2 = {
    func: sinon.stub()
  };
 
  const module = proxyquire('module', {
    './dep1': dep1,
    '../utils/dep2': dep2
  });
  return {
    module,
    dep1,
    dep2
  };
}

test('external dependency should work', t => {
  // the module itself should be exported in the first position
  // note that if you do not need any dependency stub, but just the module
  // you can simply do const {module} = getModuleMock();
  const {
    module,
    dep1,
    dep2
  } = getModuleMock();
  // test specific behavior of a dependency must be local to the test itself
  // but if it never change and is really obvious it may be defined on the factory
  dep2.func.withArgs('foo').yields('bar');
  const test = module.doSomething();
  t.true(dep1.calledOnce());
  t.true(dep2.func.calledOnce());
  t.is(test, 'bar');
}

Redux Actions

import test from 'ava';
import sinon from 'sinon';
import proxyquire from 'proxyquire';
// ensure we don't get any module from the cache, but to load it fresh every time
proxyquire.noPreserveCache();

async function testAsyncAction(action, state) {
  const fakeDispatch = sinon.spy();
  const fakeGetState = () => state;
  await action(fakeDispatch, fakeGetState);
  return fakeDispatch;
}
 
// fake module pattern with mock dependencies
function getActionsMock() {
  const fetch = sinon.stub();
  const actions = proxyquire('../actions', {
    'fetch': fetch
  });
  return {
    actions,
    fetch
  };
}
 
t('some action', t => {
  const {actions} = getActionsMock();
  const action = actions.someAction(args);
  const expectedAction = {
    type: 'SOME_ACTION',
    args: 'SOME_ARGS'
  }
   
  t.deepEqual(action, expectedAction);
})
 
t('some async action', async t => {
  const {
    actions,
    fetch
  } = getActionsMock();
  const state = {
    data: 'some initial data'
  }
 
  fetch.returns(Promise.resolve('fake data'))
  
  // use try/catch if error handling is needed
  const dispatch = await testAsyncAction(actions.someAction(), state)
  t.true(dispatch.calledWith(expectedAction));
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment