Unit Test là gì?
Unit là một thành phần nhỏ nhất mà ta có thể kiểm tra được, ví dụ như các hàm (Function), thủ tục (Procedure), lớp (Class), hoặc các phương thức (Method).
Các Unit được chọn để kiểm tra thường có kích thước nhỏ và chức năng hoạt động đơn giản; do đó chúng ta không khó khăn gì trong việc tổ chức, kiểm tra, ghi nhận và phân tích kết quả kiểm tra. Điều này giúp cho việc phát hiện lỗi sẽ dễ dàng hơn, giúp xác định nguyên nhân và khắc phục cũng tương đối dễ dàng vì chỉ khoanh vùng trong một Unit đang kiểm tra.
Mỗi Unit Test sẽ tiến hành gửi đi một thông điệp và kiểm tra câu trả lời nhận được đúng hay không, bao gồm:
- Các kết quả trả về mong muốn
- Các lỗi ngoại lệ mong muốn
Các đoạn mã Unit Test sẽ hoạt động liên tục hoặc định kỳ để thăm dò và phát hiện các lỗi kỹ thuật trong suốt quá trình phát triển. Nhờ vào đó, Unit Test còn được gọi là kỹ thuật kiểm thử tự động. Unit Test (UT) có các đặc điểm sau:
- Đóng vai trò như những người sử dụng đầu tiên của hệ thống
- Chỉ có giá trị khi chúng có thể phát hiện các vấn đề tiềm ẩn hoặc lỗi kỹ thuật
Unit Test cho UI
Tại sao phải test?
Điều hiển nhiên là chúng ta viết test nhằm mục đích hạn chế được càng nhiều lõi càng tốt,; quá trình kiểm thử này đảm bảo những gì chúng ta viết ra chạy đúng như chúng ta mong muốn. Một vài điểm trừ khi chúng ta phải viết test
- Tốn thời gian và tương đối khó khăn (dù là lập trình viên kinh nghiệm cũng gặp không ít vất vả khi mới bắt đầu viết test)
- Test pass không có nghĩa ứng dụng, function của chúng ta chạy đúng 100%
- Đôi khi, test fail, nhưng ứng dụng, function vẫn chạy hoàn toàn bình thường
- Trong vài trường hợp đặc biệt, chạy test trong CI có thể gây tốn kém
Cần Test cái gì?
- Test các chức năng, function của ứng dụng, những gì mà user sẽ sử dụng.
- Test xem các components liệu đã render đúng hay chưa
Không Cần Test cái gì?
- Việc mà code nó hiện thực như thế nào chúng ta không quan tâm, user không quan tâm, chúng ta chỉ quan tâm đầu vào-đầu ra của một function.
- Các thư viện của người khác viết cũng là thứ không cần thiết phải test. Đó là trách nhiệm của người viết thư viện.
Thiết lập cấu hình Unit Test
Hiện tại có rất nhiều thư viện dùng để test cho JavaScript. Nhưng tài liệu này sẽ hướng dẫn mọi người dùng Jest và Enzyme để test.
Dùng Jest để Test
Jest là một khung kiểm tra JavaScript được thiết kế để đảm bảo tính đúng đắn của bất kỳ cơ sở mã JavaScript nào. Nó cho phép bạn viết các test với một API dễ tiếp cận, quen thuộc và giàu tính năng, cung cấp cho bạn kết quả nhanh chóng.
Jest được ghi chép đầy đủ, yêu cầu cấu hình nhỏ và có thể được mở rộng để phù hợp với yêu cầu của bạn.
Để biết thêm thông tin về Jest, bạn có thể tham khảo tài liệu chính thức của nó tại đây: https://jestjs.io/docs/getting-started/
- Cài đặt Jest dùng npm:
npm install --save-dev jest
- Hoặc dùng Yarn:
yarn add --dev jest
- Để dùng babel, mọi người sử dụng câu lệnh:
npm install --dev babel-jest @babel/core @babel/preset-env
- Hoặc với Yarn:
yarn add --dev babel-jest @babel/core @babel/preset-env
Dùng Enzyme để Test
Enzyme là một tiện ích dùng để test JavaScript dành cho React giúp kiểm tra đầu ra của các Thành phần React của bạn dễ dàng hơn. Bạn cũng có thể thao tác, duyệt và theo một số cách mô phỏng thời gian chạy cho đầu ra. Để biết thêm thông tin về Enzyme, bạn có thể tham khảo: https://airbnb.io/projects/enzyme/
Để cài đặt Enzyme, mọi người dùng câu lệnh:
npm i --save-dev enzyme
Hoặc với Yarn:
yarn add --save-dev enzyme
Nếu bạn đang sử dụng React bản 17.x, thì hãy sử dụng câu lệnh này để cài đặt Enzyme:
npm i @wojtekmaj/enzyme-adapter-react-17
*Cách kiểm tra phiên bản React đang dùng:
Hướng dẫn tạo dự án React mới và sử dụng Test UI
Ví dụ mẫu
Sử dụng các câu lệnh để tạo dự án React cơ bản trong Terminal như npx create-react-app “Tên dự án”.
Chúng ta cần gõ đoạn code dưới đây vào file App.js trong folder src:
import React, { useState } from 'react';
const App = () => {
const [items, setItems] = useState([]);
const [item, setItem] = useState('');
const onItemChange = (e) => {
setItem(e.target.value);
};
const addItem = (e) => {
e.preventDefault();
setItems(items.concat(item));
setItem( '')
};
const submitDisabled = !item;
return(
<div
className='ui text container'
id='app'
>
<table className='ui selectable structured large table'>
<thead>
<tr>
<th>Items</th>
</tr>
</thead>
<tbody>
{
items.map((item, idx) => (
<tr
key={idx}
>
<td>{item}</td>
</tr>
))
}
</tbody>
<tfoot>
<tr>
<th>
<form
className='ui form'
onSubmit={addItem}
>
<div className='field'>
<input
className='prompt'
type='text'
placeholder='Add item...'
className='prompt'
value={item}
onChange={onItemChange}
/>
</div>
<button
className='ui button'
type='submit'
disabled={submitDisabled}
>
Add item
</button>
</form>
</th>
</tr>
</tfoot>
</table>
</div>
)
}
export default App;
Trong file setupTest.js, bạn hãy gõ đoạn code sau để cấu hình test:
import Enzyme from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
Enzyme.configure({ adapter: new Adapter() })
Tiếp theo, chúng ta sẽ gõ đoạn code sau vào file App.test.js:
import App from './App';
import React from 'react';
import { shallow } from 'enzyme';
describe('App', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(
<App />
);
});
it('should have the `th` "Items"', () => {
expect(
wrapper.contains(<th>Items</th>)
).toBe(true);
});
it('should have a `button` element', () => {
expect(
wrapper.containsMatchingElement(
<button>Add item</button>
)
).toBe(true);
});
it('should have an `input` element', () => {
expect(
wrapper.containsMatchingElement(
<input />
)
).toBe(true);
});
it('`button` should be disabled', () => {
const button = wrapper.find('button').first();
expect(
button.props().disabled
).toBe(true);
});
});
Đây là một ứng dụng Todo List cơ bản. Bây giờ, chúng ta cùng phân tích các đoạn code ở trên. Trong phạm vi bài viết này, mình sẽ chỉ phân tích các đoạn code của file App.test.js
Đầu tiên, chúng ta sẽ Import các thành phần cần thiết cho test như component App, shallow của enzyme.
import App from './App';
import React from 'react';
import { shallow } from 'enzyme';
Shallow Rendering khá là hữu ích để hạn chế bạn kiểm tra một component như một unit và để đảm bảo rằng test của bạn không gián tiếp xác nhận về hành vi của các child component.
Có thể hiểu nôm na là shallow rendering không tác động đến child component.
Ví dụ:
import { shallow } from 'enzyme';
const wrapper = shallow(<MyComponent><ChildComponent /></MyComponent>);
Thì bạn không thể find child component trong wrapper được, hơn thế nữa, cho dù <ChildComponent />
có xảy ra lỗi thì wrapper vẫn được build mà không có thông báo nào cả.
Cấu trúc của một file Test
Cấu trúc một file test:
- describe: được sử dụng để nhóm test case và mô tả hành vi của functions/modules/class. Nó nhận hai tham số; tham số đầu tiên mô tả nhóm của bạn và có kiểu dữ liệu là string.; tham số thứ hai là một function callback trong đó bạn có test case hoặc hook functions.
- it, test: đây là test case, đơn vị test của bạn. Nó phải được mô tả, và các tham số phải chính xác như describe.
- beforeAll (afterAll): hook function chạy trước (sau) tất cả test. Nó nhận một tham số và function của bạn chạy trước (sau) tất cả test.
- beforeEach(afterEach): hook function chạy trước (sau) mỗi test. Nó nhận một tham số và function của bạn chạy trước (sau) mỗi test.
Tiếp tục với ví dụ ở trên, ban đầu, dòng code dưới đây để khai báo 1 biến wrapper sẽ dùng cho toàn bộ file test để không cần phải gọi lại khi muốn test component đó:
let wrapper;
beforeEach(() => {
wrapper = shallow(
<App />
);
});
Với mỗi một it
là một test case. Chúng ta cùng phân tích test case đầu tiên:
it('should have the `th` "Items"', () => {expect(
wrapper.contains(<th>Items</th>)
).toBe(true);
});
Với case này, chúng ta sẽ kiểm tra trong component App có chứa thẻ th
với nội dung là Items hay không. Ở đây, chúng ta sẽ có các trường hợp:
- Test sẽ pass, khi component App chứa
<th>Items</th>
- Test sẽ fail, khi component App không chứa
<th>Items</th>
- Test sẽ fail, khi chúng ta truyền false vào toBe nếu như component App chứa
<th>Items</th>
Với case này, chúng ta sẽ test xem trong component App có chứa thẻ button với nội dung là Add item hay không.
it('should have a `button` element', () => {
expect(
wrapper.containsMatchingElement(
<button>Add item</button>
)
).toBe(true);
});
Tương tự với 2 test case trên, test case thứ 3 này cũng chỉ kiểm tra xem trong component App có chứa thẻ input
hay không.
it('should have an `input` element', () => {
expect(
wrapper.containsMatchingElement(
<input />
)
).toBe(true);
});
Với test case cuối, chúng ta sẽ kiểm tra xem một thẻ button đầu tiên có thuộc tính disable hay không.
it('`button` should be disabled', () => {
const button = wrapper.find('button').first();
expect(
button.props().disabled
).toBe(true);
});
Chúng ta sẽ cùng xem có .first()
và không có .first()
thì sẽ ra sao:
Cùng quan sát, chúng ta thấy khi chỉ có 1 button, thì test vẫn sẽ pass. Đó là khi CHỈ CÓ MỘT BUTTON. Còn nếu như chúng ta thêm một button khác vào thì test sẽ fail.
Nếu chúng ta thêm .first()
vào sau khi đã find được thẻ button đó, thì test sẽ pass vì nó chỉ kiểm tra button đầu tiên trong component App. Test sẽ lỗi khi button đầu tiên không có props disable.
Cách đo coverage test
Chúng ta chạy câu lệnh:
npm run test -- --coverage
Màn hình terminal sẽ hiển thị như sau:
Trên đây là hướng dẫn và ví dụ mẫu về Unit Test. Dogoo hy vọng bài viết sẽ giúp ích cho bạn!
Dogoo Team