Skip to content

itsalb3rt/unit-test-with-jest-react

Repository files navigation

Unit Test React with Jest

This is a general purpose repository for integrated Jest with react projects.

The main idea is to cover the basic and elemental aspects of unit testing with industry standards for good practice and community support.

Table of content

Get Started

React

1 -Install jest in your project and global.

⚠️ Check first if your app using react-script dependency for install Jest version according to your react-script version.

$ npm install jest --save-dev
# And global install
$ npm install -g jest

Now you need install enzyme airbnb test tool and enzyme adapter

$ npm install jest enzyme enzyme-adapter-react-16 --save-dev

Note this is exclusive for react 16 enzyme docs

2 - Now go to you src dir and created the __test__ dir and setupTest.js with the init configutation.

// src/__test__/setupTest.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

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

3 - Add jest command to package.json.

// package.json
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  },

and in the bottom of package.json added the following configuration.

This is for jest to recognize the enzyme adapters we add

// package.json
  "jest":{
    "setupFilesAfterEnv":[
      "<rootDir>/src/__test__/setupTest.js"
    ]
  }

4 - Added __mocks__ dir

Jest does not know how to handle the style files that our application has, that is why we will create a mock to prevent our tests on components with styles from failing.

create a dir __mocks__ insede src and created the styleMock.js

// src/__mocks__/styleMock.js
module.exports = {};

Finally add configuration in package.json to tell jest to use this mock.

  "jest":{
    "moduleNameMapper": {
      "\\.(styl|css)$": "<rootDir>/src/__mocks__/styleMock.js"
    }
  }

Directory Structure

The structure will always be given by our project, if we have a directory called components we must create this inside the __test__ directory;

├──src
    ├──components # Your component dir
    ├──__mocks__ # mocks information
    ├──__test__
          ├──components # Your test component dir

Anatomy of test

The basic anatomy of test is separate the test in describe sections.

// src/__test__/components/PrintString.js
const text = 'Hello World';

// The fist param in test method is a test description, this show in console when test is running

describe('String verification', () => {

  test('Check if contains a text', () => {
      // 
      expect(text).toMatch(/World/)
  });

});

Redux

For work with Redux create a new Mock with name ProviderMock.js.

//src/__mocks__/ProviderMock.js
import React from 'react';
import { createStore } from 'redux';
import { Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createBrowserHistory } from 'history';

// Import the store init state
import initialState from '../initialState';

// You reducers
import reducer from '../reducers';

const store = createStore(reducer, initialState);
const history = createBrowserHistory();

const ProviderMock = props => (
    <Provider store={store}>
        <Router history={history}>
            {props.children}
        </Router>
    </Provider>
);

export default ProviderMock;

This mock needs to be used in all your tests with redux

Actions

To test actions we only have to have an example of our payload and name of the action

// src/__test__/actions/actions.test.js
import actions from '../../actions';
import ProductMock from '../../__mocks__/ProductMock';

describe('Actions', () => {
    test('addToCart Action', () => {
        const payload = ProductMock; // example product object
        const expected = {
            type: 'ADD_TO_CART',
            payload,
        };
        expect(actions.addToCart(payload)).toEqual(expected);
    });
});

Original action.

// src/actions/index.js
const addToCart = payload => ({
  type: 'ADD_TO_CART',
  payload,
});

const removeFromCart = payload => ({
  type: 'REMOVE_FROM_CART',
  payload,
});

const actions = {
  addToCart,
  removeFromCart,
};

export default actions;

Reducers

The tests for reducers are similar to those for actions.

// src/__test__/reducers/reducers.test.js
import reducer from '../../reducers';
import ProductMock from '../../__mocks__/ProductMock';

describe('Reducers', () => {
    test('Return initial State', () => {
        //This "pass" because we pass an empty object to reduce in the expect
        // And the reducer return this empty object if action not exists 
        // (second param in reducer)
        expect(reducer({}, '')).toEqual({});
    });

    test('ADD_TO_CARD', () => {
        const initialState = {
            cart: []
        };
        const payload = ProductMock;
        const action = {
            type: 'ADD_TO_CART',
            payload
        };
        const expected = {
            cart: [
                ProductMock
            ]
        };

        expect(reducer(initialState,action)).toEqual(expected);
    })
});

Render

With the render tests or better known as snapshot we can make sure that our UI does not change, for this we can use a react tool.

$ npm install react-test-renderer --save-d

A test looks like this.

import React from 'react';
import { mount } from 'enzyme';
import {create} from 'react-test-renderer';
import Footer from '../../components/Footer';

describe('<Footer /> Render', ()=>{
    const footer = mount(<Footer />);    

    test('Render del component Footer', () => {
        expect(footer.length).toEqual(1); // check is component render
    });

    test('Render del titulo', () => {
        // Find a element by class name and check is text contain equeal to `any`
        expect(footer.find('.Footer-title').text()).toEqual("Any");
    });

});

describe('Footer Snapshot', () => {
    test('Footer', () => {
        const footer = create(<Footer />);
        /**
         * The first time you run the test the snapshot is created 
         * and when you run the test again just compare the snapshot with the 
         * component in these next tests
         */
        expect(footer.toJSON()).toMatchSnapshot();
    })
})

The example test use Redux

This will create a directory inside the test directory __snapshots__

If our UI changes for some reason the tests fail, then for this we only have to run a command in the console.

$ jest --updateSnapshot

Fetch

To test requests to an API we must install a development dependency that Jest provides.

$ npm install jest-fetch-mock --save-dev

Now to go you setupTest.js and add the configuration.

// src/__test__/setupTest.js
global.fetch = require('jest-fetch-mock');
// src/__test__/util/getData.test.js
import getData from '../../util/getData';

describe('Fetch API', () => {
    beforeEach(() => {
        fetch.resetMocks();
    });

    test('Request data API', () => {
        fetch.mockResponseOnce(JSON.stringify({ data: '12345' }));

        getData('https://google.com') // fill this with any, is required but not relevant
        .then(response => {
            expect(response.data).toEqual('12345')
        });

        expect(fetch.mock.calls[0][0]).toEqual('https://google.com')
    })
});

Original getData.js.

const getData = (api) => {
    return fetch(api)
        .then(response => response.json())
        .then(response => response)
        .catch(error => error)
}

export default getData;

Coverage

Check how much code of your application you have already tested.

$ jest --coverage

This generates a console report like this.

PASS src/__test__/global.test.js
PASS src/__test__/index.test.js
----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |      60 |        0 |   33.33 |      60 |
 index.js |      60 |        0 |   33.33 |      60 | 10-14
----------|---------|----------|---------|---------|-------------------

Test Suites: 2 passed, 2 total
Tests:       9 passed, 9 total
Snapshots:   0 total
Time:        8.775 s
Ran all test suites.

This also generates a directory in our project called coverage. Inside this dir you have Icov-report dir with index.html file, open this and check your coverage report with css 😄

Added coverage dir to gitignore


Optional Jest configurations and commands

Verbose

Activate jest verbose mode, to see the titles and describe of our tests.

  "jest": {
    "verbose": true,
  }

Run one test

# Local
$ npx jest src/__test__/components/Footer.test.js

# Or using jest Globally
$ jest src/__test__/components/Footer.test.js

Posible problems

Cannot use import statement outside a module.

Create babel.config.json and add @babel/preset-env present.

{
  "presets": [
    "@babel/preset-env"
  ]
}

and install package.

$ npm install jest babel-jest @babel/preset-env --save-dev

Plugin/Preset files are not allowed to export objects, only functions.

{
  "presets": [
    "@babel/preset-env",
    "@babel/react" <--- add this
  ]
}

And install @babel/preset-react preset

$ npm install @babel/preset-react --save-dev

Support for the experimental syntax 'classProperties' isn't currently enabled

When use class components is probably you received this error.

$ npm install --save-dev @babel/plugin-proposal-class-properties

Add plugin to babel.config.json.

  "plugins": [
    [
      "@babel/plugin-proposal-class-properties",
      {
        "loose": true
      }
    ]
  ]

SyntaxError: Unexpected token export

This means, that a file is not transformed through TypeScript compiler, e.g. because it is a JS file with TS syntax, or it is published to npm as uncompiled source files. Here's what you can do.

Adjust your transformIgnorePatterns whitelist:

"jest": {
  "transformIgnorePatterns": [
    "node_modules/(?!@ngrx|(?!deck.gl)|ng-dynamic)"
  ]
}

About

Example use Jest with React and Redux

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published