@meniga/jest

5.0.0-alpha.0 • Public • Published

@meniga/jest

This is a common library that adds support for running jest unit tests.

Setup

Step 1

Add "@meniga/jest" as a dependency in the package.json file found in the root of your repo and add the following scripts:

"scripts": {
	"test": "cross-env NODE_ENV=testing jest --config=./jest.config.js --colors",
	"test:watch": "cross-env NODE_ENV=testing jest --config=./jest.config.js --colors --watch",
	"test:coverage": "cross-env NODE_ENV=testing jest --config=./jest.config.js --colors --coverage"
}

Step 2

Create a basic jest.config.json file in the root of your repo which extends our base jest config file

Example

const base = require("./node_modules/@meniga/jest/jest.config.base.js")

module.exports = {
	...base,
	moduleNameMapper: {
		'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/node_modules/@meniga/jest/__mocks__/fileMock.js',
		'\\.(css|less)$': '<rootDir>/node_modules/@meniga/jest/__mocks__/styleMock.js',
	},
}

moduleNameMapper need to be relative to , therefor we currently need to set this in our project specific config.

See Jest Configuration documentation for more details on configurations.

Alternative test environment

As default, our testEnvironment is set to jsdom which means that the tests are runned on a browser-like environment (window, document, browser history etc are defined). Another option is to run on a node-like environment, which should be used if you are building a node service.

To change default test environment on your entire project, set testEnvironment: 'node' in the jest.config file. If you just want it changed for a specific file, you can add the following at the top of your test file:

/**
 * @jest-environment node
 */

Step 3

Add a babel.config.js file on the same location as your jest config file which uses the base babel config.

Example

const jestPath = './node_modules/@meniga/jest/'
const jestBabelConfig = require((jestPath + 'babel.config.base.js'))({ paths: [ jestPath + 'node_modules '] })

module.exports = api => api && api.env('testing') ? jestBabelConfig : {}

Step 4

For request tests to work with our current setup, we need to add a mock file for axios. In the project root, add a folder named __mocks__, and within it a file named axios.js. Add the following lines to that file:

import mockAxios from 'jest-mock-axios'
export default mockAxios

Running tests

Initially you need to run the following commands to get the packages installed and dependencies in place

dr init
dr build

Once that is done, run the following command in project root to run the tests

npm test

While you are actively working on tests it's recommended to instead run npm run-script test:watch, which listen to changes made in the code and update the test results without the need of rerunning the command.

Creating tests

Tests should added under a _tests folder which should be located as close to the code we want to test as possible, and should match the following filename syntax *.unit.js. For example, if you are to test a file under a containers folder, the folder structure should look like this:

├── containers
│ ├── _tests
│ │ └── myContainer.unit.js
│ └── myContainer.js\

When writing tests it's recommended to follow the Arrange-Act-Assert pattern, and write the describe/it blocks should be formulated in a way that describe says what is being tested and it what it does.

Testing containers/components

Here is an example unit test which:

  • Creates a snapshot of the component if it did not exist, otherwise test if there were any changes made to the component.
  • Calling a handler which updates a state value, which is confirmed by checking the elements new value.
  • If a specific call to server was done with expected action type.

Note: Higher order components (HOC) are not fully supported, at this time we can't test HOC components that use composeConnect for example.

Example container code:

import React from 'react'
import { compose, withState, withHandlers } from 'recompose'
import { Button, Container } from '@meniga/ui'

import * as myActions from '../actions'

const Greeter = ({ name, myState, onUpdateName, onStoreName }) => (
	<Container>
		<span class="result-container">{ myState }</span>
		<Button onClick={ () => { onUpdateName(name) } }></Button>
		<Button onClick={ onStoreName }></Button>
	</Container>
)

export const enhance = compose(
  withState('myState', 'setMyState', ""),
  withHandlers({
    onUpdateName: ({ setMyState }) => name => {
      setMyState(name)
    },
    onStoreName: ({ dispatch }) => () => {
      dispatch(myActions.fetch())
    }
  })
)

export default enhance(Greeter)

Basic example tests for example container:

import React from 'react'
import renderer from 'react-test-renderer'

import Greeter from '../Greeter'

describe('UI ', () => {
  
  it('should render a component', () => {
    const greeter = renderer.create(
      <Greeter name='Shreya Dahal' />
    ).toJSON()

    expect(greeter).toMatchSnapshot()
  })

  it('should change name displayed on click', () => {
    const name = 'Shreya Dahal'

    const greeter = renderer.create(
      <Greeter name={ name } />
    ).root

    const buttons = greeter.findAll(node => node.type === "button")
    expect(buttons.length).toBe(2)

    const nameTag1 = greeter.findAll(node => node.type === "span")
    expect(nameTag1.length).toBe(1)
    expect(nameTag1[0].children[0]).toBe("")

    renderer.act(buttons[0].props.onClick)

    const nameTag2 = greeter.findAll(node => node.type === "span")
    expect(nameTag2.length).toBe(1)
    expect(nameTag2[0].children[0]).toBe(name)
  })
})

Adding store mock

To mock store in the above example, we would need to add the following imports:

import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'
import promiseMiddleware from 'redux-promise-middleware'
import thunk from 'redux-thunk'

const middlewares = [thunk, promiseMiddleware()]
const mockStore = configureStore(middlewares)

and modify the test renderer instance creator as follows

const myStore = mockStore({
  challenges: { items: [] },
  categories: { items: [] }
})

const greeter = renderer.create(
  <Provider store={ myStore }>    
    <Greeter name={ name } />
  </Provider>
).root

Mocking endpoints

To mock endpoints we're using jest-mock-axios.

Basically to start mocking you need to add the following to your test file:

import mockAxios from 'jest-mock-axios'

jest.mock('axios')

To reset it between tests, add

afterEach(() => {
  mockAxios.reset()
})

Use mockResponse or mockResponseFor to simulate a successful server response, mockError to simulate error (non-2xx). Note that these should be added after the action has been called.

mockAxios.mockResponseFor({ url: 'api.baseUrl/update' }, { data: {} }) // Successful example
mockAxios.mockError({ data: {} }) // Error example

Example on how to test that the request was made

expect(mockAxios.post).toHaveBeenCalledWith('api.baseUrl/update')

The library also provide functions to get information about the requests that were made which are helpful in pinpointing exactly which requests we want to resolve if there are multiple requests made.

Testing selectors

Basic testing of selectors is a piece of cake all thanks to the resultFunc provided. Below is a simple example where the challenge selector use one list of challenges and one of categories. The output list has challenges where instead of a list of category ids, the challenges objects has a list of the category names.

import { challenges } from '../selectors'

describe('Challenges ', () => {
  describe('list', () => {
    it('should return a challenge with a list of category names matching provided category ids', () => {
      // Arrange
      const challengesMock = [{
        typeData: {
          categoryIds: [2, 3]
        },
      }]

      const categoriesMock = [
        { id: 2, name: "myFirstCategory" },
        { id: 3, name: "mySecondCategory" },
        { id: 7, name: "myThirdCategory" }
      ]

      const expectedResult = categoriesMock.filter(c => challengesMock[0].typeData.categoryIds.includes(c.id)).map(c => c.name)

      // Act
      const selected = challenges.resultFunc(challengesMock, categoriesMock)

      // Assert
      expect(selected[0].categoryNames).toEqual(expectedResult)
    })
  })
})

Generating code coverage reports

By default, coverageThreshold is configured to require 70% (as 70-80% is generally recognized as goal). Until we are able to meet that requirement extended jest configs need to override this to what is currently feasible.

coverageThreshold: {
  global: {
    branches: 0,
    functions: 0,
    lines: 0,
    statements: 0,
  },
}

It's also possible to set folder/file specific requirements which can be useful until we have been able to reach the goal in order to ensure our well-tested sections are kept that way.

Run npm run-script test:coverage to collect code coverage.

Help

I'm changing the jest's transform related config but it doesn't seem to do anything?

Add --no-cache to npm test

Readme

Keywords

none

Package Sidebar

Install

npm i @meniga/jest

Weekly Downloads

1

Version

5.0.0-alpha.0

License

MIT

Unpacked Size

19.8 kB

Total Files

8

Last publish

Collaborators

  • meniga-npm
  • petermeniga
  • tinna