@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\
- View Jest's API Reference section for more details on how to create basic tests
- View React's documentation about react-test-renderer for the basics of how to test a container/component.
- View redux documentation regarding testing redux related code
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