Test ứng dụng React Native với Jest (Phần 1)
Mở đầu
Unit test là bước đầu tiên trong quy trình kiểm thử phần mềm. Hãy xem mô hình dưới đây để thấy được tầm quan trọng của Unit tests. Khi càng tăng test ở unit tests sẽ càng giảm test ở các tầng trên.
Trong unit test ta sẽ kiểm tra từng phần nhỏ trong code, đó có thể là các method, function trong class, thậm chí là phần nhỏ hơn trong function.
Hôm nay tôi sẽ giới thiệu với các bạn framework của React Native giúp thực hiện công việc test đó chính là Jest.
1.Configure Jest
Từ phiên bản RN 0.38 trở lên, Jest đã được tích hợp sẵn khi bạn tạo project bằng react-native init
, vì thế việc setup này đối với bạn có thể đã có sẵn và ko cần setup thêm nữa. Tham khảo cách thêm cách cài đặt tại đây
Kiểm tra file package.json
{
"scripts": {
"test": "jest"
},
"devDependencies": {
"babel-jest": "24.9.0",
"jest": "24.9.0",
"react-test-renderer": "16.9.0"
},
"jest": {
"preset": "react-native"
}
}
2.Snapshot testing📸 là gì?
Với Jest chúng ta có thể dễ dàng test các component khi có sự thay đổi props và state.
Snapshot test là công cụ rất hữu ích trong trường hợp bạn muốn chắc chắn rằng UI không bị thay đổi ngoài ý muốn.
Trong react native , snapshot testing là việc tạo ra 1 file snapshot, sau đó trong những lần test sau, các component sẽ tiếp tục tạo ra các rendered output khác để so sánh với file snapshot ban đầu, nếu có sự thay đổi thì kết quả test sẽ fail.
3.Ứng dụng snapshot test vào project
Sau khi tạo project bằng câu lệnh react-native init
ta được cấu trúc thư mục sau:
Ở đây tôi đã thêm thư mục src
dùng để chứa file source trong project. Trong thư mục này, tôi sẽ tạo thêm 1 file Button.js
import React, { Component } from 'react';
import { Text, TouchableOpacity, Linking } from 'react-native';
// 1. Changed to a class based component
class Button extends Component {
constructor(props) {
super(props);
}
// 2. Custom function called onPress TouchableOpacity
onPressHandler = () => {
const { onPress, url } = this.props
if (url) {
Linking.openURL(url)
}
onPress();
}
render() {
const { buttonStyle, textStyle } = styles;
const { label } = this.props;
return (
<TouchableOpacity onPress={this.onPressHandler} style={newButtonStyle}>
<Text style={textStyle}>
{label}
</Text>
</TouchableOpacity>
);
}
};
const styles = {
textStyle: {
alignSelf: 'center',
color: '#fff',
fontSize: 16
},
buttonStyle: {
height: 45,
justifyContent: 'center',
backgroundColor: '#38ba7d',
borderBottomWidth: 6,
borderBottomColor: '#1e6343',
borderWidth: 1,
marginLeft: 10,
marginRight: 10
}
};
export default Button;
Kết quả Button sẽ giống như thế này:
Như vậy đã xong phần chuẩn bị, bây giờ chúng ta bắt đầu viết test.
Đầu tiên tạo file Button-test.js
trong thư mục _tests_
import 'react-native';
import React from 'react';
import Button from '../src/Button';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
describe('Button', () => {
describe('Rendering', () => {
it('should match to snapshot', () => {
const component = renderer.create(
<Button label={'Click me!'}/>
).toJSON();
expect(component).toMatchSnapshot()
});
});
});
Ở đây cần chú ý đến react-test-renderer.
'react-test-renderer'
là một thư viện giúp chúng ta có thể tạo ra một snapshot của DOM tree được render bởi React DOM or React Native component mà không cần sử dụng browser hoặc jsdom.
Thay vì phải render ra các object thật thì nó render ra các JS object để có thể thực hiện test trực tiếp trên Node.
Cấu trúc hàm test:
describe('Button', () => {
// test stuff
}
Method describe
chứa một hoặc nhiều test liên quan. Mỗi lần viết một test mới hãy bọc nó trong describe block.
it('should match to snapshot', () => {
// actual test
});
Hàm test được truyền vào callback như một tham số trong hàm it
. Ngoài ra ta truyền thêm phần mô tả mong muốn hàm thực hiện.
Để có thể test được chúng ta cần chuẩn bị dữ liệu đầu vào(input), dữ liệu đầu ra(output) và hàm so sánh(function) 2 kết quả có trùng khớp nhau không.
Ở ví dụ trên ta có component
đóng vai trò là input data (snapshot của DOM tree).
Hàm expect
cho phép chúng ta truy cập vào các phương thức matcher
để phát hiện ra những sự sai khác. Ở đây chúng ta sử dụng toMatchSnapshot
để đảm bảo rằng nó trùng với snapshot gần đây nhất.
Running test: npm test
để xem điều gì xảy ra.
Khi chạy snapshot test lần đầu, Jest sẽ tạo một snapshot file trong thư mục _snapshots_
Khi mở file snapshot sẽ thấy nó convert view của chúng ta sang object để tiện so sánh cũng như test.
Điều gì xảy ra nếu chúng ta thay đổi UI
Thay đổi màu nền button backgroundColor: '#0000FF'
trong buttonStyle
, và chạy lại test npm test
Ngay lập tức jest báo kết quả test bị FAIL
, như đã trình bày ở trên việc thay đổi code dẫn đến bản snapshot
mới khác so với bản cũ. Jest còn chỉ ra chỗ thay đổi, thật sự rất tiện lợi.
Vậy nếu muốn cập nhật bản snapshot
mới thì sao. Rất đơn giản sử dụng lệnh sau:
npm test -- -u
Kết quả snapshot
đã được update lại. Lời khuyên nên commit
lên git mỗi khi update lại snapshot
.
Ngoài ra còn có các option khác:
4. Matchers trong Jest
expect(2 + 2).toBe(4);
.toBe()
là một matcher, dùng để so sánh kết quả thực hiện với kết quả mong muốn.
.toBe()
sử dụng Object.is
để test. Khi muốn kiểm tra giá trị của object hãy sử dụng toEqual
. Ví dụ:
it('object assignment', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
Nếu muốn so sánh kết quả trái với mong đợi sử dụng .not.toBe()
it('adding positive numbers is not zero', () => {
for (let a = 1; a < 10; a++) {
for (let b = 1; b < 10; b++) {
expect(a + b).not.toBe(0);
}
}
});
Ngoài ra còn có các matches khác.
Truthiness
toBeNull
so sánh với giá trịnull
toBeUndefined
so sánh với giá trịundefined
toBeDefined
là hàm cho kết quả ngược lạitoBeUndefined
.toBeTruthy
so sánh với giá trịtrue
.toBeFalsy
so sánh với giá trịfalse
.
Numbers
it('two plus two', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
// cả toBe và toEqual đều có thể sử dụng cho numbers
expect(value).toBe(4);
expect(value).toEqual(4);
});
Đối với số thực sử dụng toBeCloseTo
thay vì toEqual
it('adding floating point numbers', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); This won't work because of rounding error
expect(value).toBeCloseTo(0.3); // This works.
});
Strings
Có thể kiểm tra strings với regular expressions bằngtoMatch
it('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
it('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
Arrays và Iterables
Kiểm tra một giá trị có trong Array hay Iterable không bằng cách toContain
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'beer',
];
it('the shopping list has beer on it', () => {
expect(shoppingList).toContain('beer');
expect(new Set(shoppingList)).toContain('beer');
});
Exceptions
Để kiểm tra một lỗi có thể xảy ra bạn có thể sử dụng toThrow
:
function compileAndroidCode() {
throw new Error('you are using the wrong JDK');
}
it('compiling android goes as expected', () => {
expect(compileAndroidCode).toThrow();
expect(compileAndroidCode).toThrow(Error);
// You can also use the exact error message or a regexp
expect(compileAndroidCode).toThrow('you are using the wrong JDK');
expect(compileAndroidCode).toThrow(/JDK/);
});
5. Mock Functions
Mock giúp bạn mô phỏng các tính chất và hành vi giống hệt như đối tượng thực nhằm kiểm tra tính đúng đắn của các hoạt động bên trong.
Quay lại với ví dụ button, bây giờ chúng ta sẽ test xem sự kiện button có được kích hoạt không
describe('onPressHandler', () => {
it('should call onPress', () => {
const mockOnPress = jest.fn(); // 1. mock function
const component = renderer.create(<Button
label= "test label"
onPress={mockOnPress} // 2. passing in mock function as props
/>)
const instance = component.getInstance(); // 3. getting an instance of component
instance.onPressHandler(); // 4. manually triggering onPressHandler()
// Act
expect(mockOnPress).toHaveBeenCalled();
});
});
});
Chạy lại lệnh npm test
để xem kết quả:
Bài viết sau tôi sẽ giới thiệu chi tiết hơn về Mock( mock function, class mock, mock modules, ...)