How to test your react redux application
Bài đăng này đã không được cập nhật trong 6 năm
Bài viết này mình sẽ hướng dẫn cách test redux app với Jest và Enzyme.
Cài đặt Jest và Enzyme
- Jest:
yarn add --dev jest
hoặcnpm install --save-dev jest
- Enzyme: tuỳ thuộc vào phiên bản react hiện tại, bạn cài đặt enzyme cùng với adapter thích hợp. Ví dụ phiên bản react trong project là 16:
yarn add --dev enzyme enzyme-adapter-react-16
hoặcnpm i --save-dev enzyme enzyme-adapter-react-16
- Chú ý: bạn nên cài thêm package babel-jest để jest compile JS sử dụng babel (
npm install --save-dev babel-jest babel-core
hoặcyarn add --dev babel-jest babel-core
)
Reducer test
Reducer là pure function, nó lấy state có sẵn và một action để trả về state mới sau khi action được gọi, vì thế sẽ k có side effect nên mocking ở đây là không cần thiết.
Jest mặc định sẽ tìm kiếm các file test trong thư mục __test__
và extension có dạng .spec.js
hoặc .test.js
. Ví dụ reducer được đặt trong thư mục src/store/topics/reducer.js
thì ta có thể tạo thư mục test như sau src/__test__/store/topics/reducer.spec.js
.
Case test đơn giản nhất với reducer là khi khởi tạo, lúc này ta chỉ cần khai báo state khởi tạo mà không liên quan gì đến action:
import uut from '../store/topics/reducer';
const initialState = {
topicsByUrl: undefined,
selectedTopicUrls: [],
selectionFinalized: false,
};
describe('store/topics/reducer', () => {
it('should have initial state', () => {
expect(uut()).toEqual(initialState);
});
});
Giả sử ta thêm một action và kiểm tra state được trả về có đúng hay không:
import Immutable from 'seamless-immutable';
import { Reducer } from 'redux-testkit'; // thư viện hỗ trợ verify immutable reducer
import uut from '../store/topics/reducer';
import * as actionTypes from '../constants/actionTypes';
const initialState = {
topicsByUrl: undefined,
selectedTopicUrls: [],
selectionFinalized: false,
};
describe('store/topics/reducer', () => {
it('should have initial state', () => {
expect(uut()).toEqual(initialState);
});
it('should not affect state', () => {
Reducer(uut).expect({type: 'NOT_EXISTING'}).toReturnState(initialState);
});
it('should store fetched topics', () => {
const topicsByUrl = {url1: 'topic1', url2: 'topic2'};
const action = {type: actionTypes.TOPICS_FETCHED, topicsByUrl};
Reducer(uut).expect(action).toReturnState({...initialState, topicsByUrl});
});
it('should store fetched topics and override existing topics', () => {
const existingState = Immutable({...initialState, topicsByUrl: {url3: 'topic3'}});
const topicsByUrl = {url1: 'topic1', url2: 'topic2'};
const action = {type: actionTypes.TOPICS_FETCHED, topicsByUrl};
Reducer(uut).withState(existingState).expect(action).toReturnState({...initialState, topicsByUrl});
});
});
Cá nhân mình thấy rspec và jest test khá giống nhau, dễ đọc và hiểu từng test case :man_detective: .
Nếu state object phức tạp, redux-testkit có các method cho nested state hoặc sử dụng custom expectations. (tham khảo thêm tại đây)
Action test (middleware mặc định: thunks)
Thunks wrap sync hoặc async function để thực hiện action. Nó có thể tạo ra các side effect như truy xuất dữ liệu từ server, vậy mock function ở đây là cần thiết. Ví dụ:
import reducer from '../store/topics/reducer';
import * as actions from '../actions/posts/getPost';
import { UPDATE_POST_SUCCESS } from '../actions/posts/updatePost';
import getPostMock from '../mocks/getPostMock';
describe('post reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual({});
});
it('should handle GET_POST_START', () => {
const startAction = {
type: actions.GET_POST_START
};
// it's empty on purpose because it's just starting to fetch posts
expect(reducer({}, startAction)).toEqual({});
});
it('should handle GET_POST_SUCCESS', () => {
const successAction = {
type: actions.GET_POST_SUCCESS,
post: getPostMock.data, // important to pass correct payload, that's what the tests are for ;)
};
expect(reducer({}, successAction)).toEqual(getPostMock.data);
});
it('should handle UPDATE_POST_SUCCESS', () => {
const updateAction = {
type: UPDATE_POST_SUCCESS,
post: getPostMock.data,
};
expect(reducer({}, updateAction)).toEqual(getPostMock.data);
});
it('should handle GET_POST_FAIL', () => {
const failAction = {
type: actions.GET_POST_FAIL,
error: { success: false },
};
expect(reducer({}, failAction)).toEqual({ error: { success: false } });
});
});
Component test
-
Snapshot test Jest có chức năng snapshot testing được rất nhiều công ty ưa chuộng, ban đầu jest sẽ capture cấu trúc của component, cấu trúc này sẽ được lưu vào thư mục
__snapshot__
. Khi có bất kì thay đổi trong component đã được snapshot trước đó, test case với component được capture sẽ lỗi. Ví dụ ta có component Link:// Link.react.js import React from 'react'; const STATUS = { HOVERED: 'hovered', NORMAL: 'normal', }; export default class Link extends React.Component { constructor(props) { super(props); this._onMouseEnter = this._onMouseEnter.bind(this); this._onMouseLeave = this._onMouseLeave.bind(this); this.state = { class: STATUS.NORMAL, }; } _onMouseEnter() { this.setState({class: STATUS.HOVERED}); } _onMouseLeave() { this.setState({class: STATUS.NORMAL}); } render() { return ( <a className={this.state.class} href={this.props.page || '#'} onMouseEnter={this._onMouseEnter} onMouseLeave={this._onMouseLeave} > {this.props.children} </a> ); } }
Ta sẽ tạo snapshot bằng method
toMatchSnapshot()
:import React from 'react'; import Link from '../Link.react'; import renderer from 'react-test-renderer'; test('Link changes the class when hovered', () => { const component = renderer.create( <Link page="http://www.facebook.com">Facebook</Link>, ); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); // manually trigger the callback tree.props.onMouseEnter(); // re-rendering tree = component.toJSON(); expect(tree).toMatchSnapshot(); // manually trigger the callback tree.props.onMouseLeave(); // re-rendering tree = component.toJSON(); expect(tree).toMatchSnapshot(); });
Nếu bạn chủ động cho phép thay đổi cấu trúc trong component, bạn cần update snapshot cho component bằng câu lệnh
jest -u
. -
DOM test sử dụng enzyme shallow renderer : Được sử dụng khi bạn muốn test các element trong DOM tree. Ví dụ bạn có component sau:
// CheckboxWithLabel.js import React from 'react'; export default class CheckboxWithLabel extends React.Component { constructor(props) { super(props); this.state = {isChecked: false}; // bind manually because React class components don't auto-bind // http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding this.onChange = this.onChange.bind(this); } onChange() { this.setState({isChecked: !this.state.isChecked}); } render() { return ( <label> <input type="checkbox" checked={this.state.isChecked} onChange={this.onChange} /> {this.state.isChecked ? this.props.labelOn : this.props.labelOff} </label> ); } }
Component sẽ được test như sau:
// __tests__/CheckboxWithLabel-test.js import React from 'react'; import {shallow} from 'enzyme'; import CheckboxWithLabel from '../CheckboxWithLabel'; test('CheckboxWithLabel changes the text after click', () => { // Render a checkbox with label in the document const checkbox = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />); expect(checkbox.text()).toEqual('Off'); checkbox.find('input').simulate('change'); expect(checkbox.text()).toEqual('On'); });
Kết
Hy vọng bài viết một phần giúp bạn hình dung ra unit test của react redux. Nếu có bất kì thắc mắc, hãy comment bên dưới
All rights reserved