JEST & Nock Testing

Let’s go through the process of setting up JEST with our React application, writing our first test and mocking an api call.

Setup

First we’ll need to install dependencies via yarn

yarn add enzyme
yarn add enzyme-adapter-react-16
yarn add jest
yarn add jest-enzyme
yarn add nock

And add jest to our package.json to fire up JEST

"scripts": {
  "start": "webpack-dev-server --mode development --open --hot --progress",
  "build": "webpack --mode production",
  "test": "jest"
},

We’ll also need a JEST setup file to load the enzyme adapter for React

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

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

File Layout

__mockData__/[mock json files]
__mocks__/[test files]
jest.setup.js
package.json

Mock Data

Rather than interact with live data we need to mock any data we give our tests so we have a staple ground with which to interact with our application.

We’ll do this quite simply by storing our mock data in JSON files and pulling it in much like how our api would send it.

// __mockData__/videos.json
[
  {
    "id": "01"
    "title": "Avengers Infinity War"
  },
  {
    "id": "02"
    "title": "Avengers Endgame"
  }
]

#

Now lets move to writing our first test. For this example we’re going to use a simple Async request and then use Nock to intercept the GET request and give it the data from our videos.json file.

First our sample API call

//../src/videoRequest.js
import request from 'superagent';

async function listVideos(category) {
  return await request
    .get(`/v1/videos/category/${category}`)
    .set('accept', 'json')
    .then(function(response) {
      return response.body
  });
}
export {
  listVideos
}

And now our Test

// __tests__/testAPI.js
import React, { component } from 'react';
import { mount, shallow } from 'enzyme';
import toJSON from 'enzyme-to-json';
import nock from 'nock';

// load helper get method for video categories
import listVideos from './../src/videoRequest';

// load mock data
let mockVideos = require('./../__mockData__/videos.json');

// setup get intercepts,
// specifying the API path and data to return.
nock('http://localhost')
  .get('/v1/videos/')
  .reply(200, { mockVideos });

nock('http://localhost')
  .get('/v1/videos/category/all')
  .reply(200, { mockVideos });

// describe tests
describe('videoRequest Component', () => {
  describe('when helpers fired', () => {
    // define test, applying async similar to the live version
    it('return videos in category all', async () => {
      // fire get request
      const results = await listVideos('all')
      // define expected returned data
      expect(results.mockVideos[0].title).toEqual("Avengers Infinity War")
    })
  })
});

Notice at the start we load the mock data into memory.

let mockVideos = require('./../__mockData__/videos.json');

Then knoking the API route the request will use we tell nock to intercept that very path and method returning it that very same data.

nock('http://localhost')
  .get('/v1/videos/category/all')
  .reply(200, { mockVideos });

That way when we test the data give we can have predictable results.

expect(results.mockVideos[0].title).toEqual("Avengers Infinity War")

We can use this same method to mock other GET requests, POST requests, error handling for front-end so we’re able to test out our components without them touching live data.

React AutoComplete, Async & Aggregates

Recently I had to create an autocomplete component for a React project, the difference here is that even though the data was coming from the popular NoSQL datastore MongoDB I had to pull it from several sources.

With Mongo and Node.JS API calls you only really have one hit you can use in order to request all the data you need for the request.

Now you can use Async with Mongoose to recall several tables at once:

Server: With Async

var team = require('../../models/team.js');
var user = require('../../models/user.js');
var async = require('async');

try {
  async.parallel({
    teams: function(cb){
      team.find({}, '_id title')
      .lean() // cut out virtuals
      .exec(cb)
    },
    users: function(cb){
      user.find({}, '_id email')
      .lean() // cut out virtuals
      .exec(cb)
    }
  }, function(err, results){

    const teams = results.teams // store teams data
    const users = results.users // store users data

    // and then process the results

But for autocomplete it seems impractical to use, so we’ll be using Mongoose’s aggregate feature link and return just the data we need in the format we want.

Client: AutoComplete Component

First we’ll create a React component to autocomplete team names, we’ll use the isomorphic-fetch component to trigger the call. Using React-Select to render the results and using props to pass back and forth the selected data to the parent.

Here the autocomplete component returns all teams with their assigned user.

import React, { Component } from 'react';
import Select from 'react-select';
import fetch from 'isomorphic-fetch';

class AutoCompleteTeams extends Component {
  // define initial props
  static defaultProps = {
    className: '',
    update: null,
    initialValue: { _id: '', title: '' }
  }

  // define initial state
  constructor(props) {
    super(props);
    this.state = {
      selectedArray: new Object()
    }
  }

  componentWillReceiveProps(newProps){
    if (newProps === this.props) {
      this.setState({
        selectedArray: new Object()
      }, () => {
        return;
      })
    }

    this.setState({
      selectedArray: newProps.initialValue || new Object()
    }, function() {
    })
  }

  // onChange (item selected), return value to parent
  // => { title: 'Alexandria (john@smith.com)' }
  onChange = (value) => {
        this.setState({
      selectedArray: value,
        }, function() {
      this.props.update(value);
    });
    }

  // on autocomplete, perform GET request. 
  // We'll also send credentials as we're operating
  // within a secure session
  autocomplete = (input) => {
    if (!input) {
            return Promise.resolve({ options: [] });
    }

    return fetch(`/v1/users/autocomplete/teams?q=${input}`, {
      method: 'GET',
      credentials: 'include' })
      .then((response) => response.json())
      .then((json) => {
        return { options: json };
      });
  }

  // render autocomplete using Select.Async displaying
  render() {
    const { className} = this.props
    return (
       <div className={`form-control ${className}`}>
         <Select.Async
           value={this.state.selectedArray}
           onChange={this.onChange}
           valueKey="_id" // define which value to use when item selected
           labelKey="title" // define which field to display in select
           loadOptions={this.autocomplete}
           backspaceRemove={true}
         />
       </div>
    )
  }
}

export default AutoCompleteTeams;

Client: Parent Usage

We can then add it to our parent component via:

import React, { Component } from 'react';
import AutoCompleteTeams from './../sharedComponents/AutoCompleteTeams'

class AddTeamLeader extends Component {

  setTeam = (value) => {
    this.setState({ user: value })
  }

  render() {
    return (<div>
      <AutoCompleteTeams
        className="col-xs-12 col-sm-6 col-md-8"
        name="title"
        id="title"
        initialValue={this.state.team}
        update={this.setTeam}
        />
      </div>)
  }
}

Server: Aggregate Query

Now we’ll define the get request, using aggregation to return a list of teams with their respective leader’s email in brackets.

var team = require('../../models/team.js');

exports.autocompleteTeams = function(req, res) {
  const q = req.query.q || ''
  if (q) {
    team.aggregate([
      // Order is important, each extra match drills down the results available.
      {$lookup: {from: 'users', localField: 'leader', foreignField: '_id', as: 'leader'} },
      {$project: {"title" :
        { $concat : [
          "$title",
          " (",
          { $arrayElemAt:["$leader.email", 0] },
          ")"
        ] }
      }},
      {$match: {"title": { "$regex": q, "$options": "i" }}},
      {$sort: {"title": 1}}
    ])
    .limit(25)
    .then((records) => res.send(records))
  } else {
    team.aggregate([
      {$lookup: {from: 'users', localField: 'leader', foreignField: '_id', as: 'leader'} },
      {$project: {"title" :
        { $concat : [
          "$title",
          " (",
          { $arrayElemAt:["$leader.email", 0] },
          ")"
        ] }
      }},
      {$sort: {"title": 1}}
    ])
    .limit(25)
    .then((records) => res.send(records))
  }
}

Lets drill down the aggregate action, as we cannot access virtuals inside mongoose queries we cannot pull this data from a populate() action and then pass it into our records in one so we do a look up connecting team to users via it’s assigned ‘leader’ field which will match the user field _id.

{$lookup: {from: 'users', localField: 'leader', foreignField: '_id', as: 'leader'} },

Next with the user connected to team we project or create the title value concatenating the email address to the end in brackets.

{$project: {"title" :
  { $concat : [
    "$title",
    " (",
    { $arrayElemAt:["$leader.email", 0] },
    ")"
  ] }
}},

Then we match the result to the API request value.

{$match: {"title": { "$regex": q, "$options": "i" }}},

Sort the results.

{$sort: {"title": 1}}

Giving us the end output: