React with redux-saga Testing
Testing ứng dụng của bạn là một phần quan trọng đối với sự phát triển phần mềm chuyên nghiệp. Có 1 vài thứ bạn nên test. Một vài kỹ thuật được sử dụng để test tôi xin giới thiệu khi test ứng dụng react kết hợp redux, saga là Unit Testing, Snapshot Testing, Shallow rendering.
Note: Framework sử dụng demo
- Ứng dụng React kết hợp redux-saga: Để cho đơn giản, tôi sử dụng React Boilerplate để demo vì nó đã được setup một ứng dụng đầy đủ với các kỹ thuật đã kể trên.
- Framework test: JEST kết hợp Sinon
Trước tiên chúng ta liệt kê qua một số thành phần cần test có trong ứng dụng React, redux-saga:
- Action.
- Reducer: Nơi tiếp nhận action và xử lý.
- Saga: middleware.
- React component.
Các kỹ thuật testing
Unit testing
Unit testing là 1 thực hành việc test với đơn vị nhỏ nhất có thể trong code và các hàm của chúng ta. Chúng ta chạy các test và kiểm tra tự động các hàm của chúng ta làm việc có đúng như chúng ta mong đợi chúng làm. Chúng ta assert (yêu cầu, đòi hỏi) rằng gửi 1 tập đầu vào và hàm của chúng ta trả về các giá trị phù hợp và xử lý các vấn đề.
Boilerplate đã hỗ trợ sẵn Jest test framework để chạy test và tạo xác sự xác nhận. Thư viện làm cho chúng ta viết test đơn giản như các lời nói: bạn mô tả (describe
) 1 đơn vị trong code của bạn và mong đợi (expect
) nó (it
) làm việc gì đó đúng:
1 | describe('add()', () => { |
Snapshot Testing
Đây là một kỹ thuật rất hữu ích được JEST hỗ trợ để test bất cứ khi nào bạn muốn đảm bảo rằng giao diện người dùng (UI) của bạn không có các thay đổi bất ngờ.
Ý tưởng của kỹ thuật này là với mỗi test case, bạn sẽ renders các component, ghi nhận lại 1 snapshot và so sánh nó với snapshot đã được lưu lại từ trước với cùng test case đó. Test không thành công nếu 2 snapshot không khớp nhau. Tuy nhiên trong trường hợp 2 snapshot không khớp nhau, nếu bạn thấy sự thay đổi đó là đúng như cái bạn dự kiến (không bất ngờ) thì bạn có thể cập nhật snapshot, chạy lại để test case trở thành đúng là cập nhật phiên bản snapshot này cho các lần test sau đó :D
Kỹ thuật này không chỉ được sử dụng để test UI, nó có thể được sử dụng để test saga, lưu các trạng thái của action, chúng ta sẽ xem xét nó chi tiết trong phần Saga Testing.
Shallow rendering
Kỹ thuật này tạm dịch là render “nông”. Khi bạn chỉ muốn viết unit test cho React, kỹ thuật này có thể hữu ích. Kỹ thuật cho phép bạn kết xuất một thành phần sâu ở một cấp độ (“one level deep”) và xác nhận giá trị trả về từ phương thức render của nó trả về mà không phải lo lắng về các hành vi của các thành phần con không được khởi tạo hoặc hiển thị. Điều này có nghĩa, nó không yêu cầu DOM.
Kỹ thuật này có thể được coi là unit test cho các component của React (chỉ quan tâm kết quả trả về hàm render, không quan tâm các hành vi và thẻ con). Tuy nhiên đó cũng chính là điểm yếu của nó
- Vì không render ra DOM nên nó không thể test được các hành vi với các component (click, change …)
- Bạn phải thực hiện test thủ công với từng commponent
Kỹ thuật này cá nhân mình thấy ít được sử dụng. Để test các component trong React, kỹ thuật snapshot testing với sự giúp đỡ của 1 vài thư viện có vẻ hữu ích hơn.
Action, reducer testing
Reducer
Với reducer bạn có thể sử dụng unit test hoặc snapshot để test. Unit test được sử dụng nếu state ít thay đổi (để tránh cập nhập code liên tục), nếu không, hãy cân nhắc sử dụng snapshot testing.
Ví dụ khi test init state reducer:
1 | describe('NavBarReducer', () => { |
Với init state là:
1 | { |
Tuy nhiên phương pháp test này có 1 điểm yếu là khi có 1 ai đó thay đổi init state => bạn phải thay đổi code??
Thay vào đó có thể sử dụng snapshot, khi ai đó thay đổi init state => báo lỗi change snapshot. Từ đó bạn có thể so sánh để cập nhật snapshot khi test mà không phải cập nhật code
1 | describe('NavBarReducer', () => { |
Actions
Một Redux action là 1 các hàm thuần túy vì vậy không khó để test nó, sử dụng unit test, truyền input và expect output thôi :D
1 | it('should return the correct constant', () => { |
Saga Testing
Có 2 cách chính để test Sagas: test hàm generator từng bước một hoặc chạy full saga và kiểm tra các effects được tạo ra từ saga
Testing the Saga Generator Function
Để test từng bước một các hàm generator bạn có thể sử dụng hàm next().value
để lấy ra các giá trị trả về từ yield, sau đó kết hợp kỹ thuật snapshot hoặc các kết quả trả về đó để so sánh với kết quả mong đợi.
Chúng ta cùng xem xét ví dụ về saga sau:
1 | /** |
Chúng ta sẽ dùng next().value
để lấy ra các giá trị từ yield và so sánh với kết quả ta mong muốn:
1 | /** |
Branching Saga
Thỉnh thoảng, saga của bạn lại có một vài kết quả khác nhau. Khi đó để test các nhánh khác nhau, bạn cần truyền giá trị trả về mong đợi từ yield trước đó vào hàm next để thực hiện vào các branch.
Chi tiết cách test này tham khảo tại đây.
Testing the full Saga
Mặc dù rất hữu ích khi test từng bước một với saga, nhưng trên thực tế nó có thể làm cho test của saga đơn giản và ngắn. Thay vào đó một cách thích hợp hơn để test cho cả saga và mong đợi các effects đã xảy ra.
Chúng ta cùng quay lại với ví dụ trên. Chúng ta sẽ test cả hành động call API lấy repos của 1 người dùng trong trường hợp thành công và mong đợi hành động put(reposLoaded(repos, username))
được xảy ra.
Trước hết chúng ta cần mock function request. Việc làm này để mô phỏng giá trị trả về thành công API thay vì ta gọi API thực tế, một kỹ thuật phổ biến được sử dụng trong test.
1 | const dispatched = []; |
Và rồi test cả saga thôi:
1 | describe('getRepos fullSaga', () => { |
Component Testing
Unit Tesing với action và reducer là ngon, nhưng bạn có thể làm thậm chí là nhiều hơn để chắc chắn rằng không gì bị break trong ứng dụng của bạn. Vì React là tầng view của ứng dụng, hãy xem xét các test Components
- Shallow rendering
- react-testing-library
- Snapshot testing
- Behavior testing
Shallow rendering
Như đã đề cập trong lý thuyết, nó là kỹ thuật Unit Test với các component của React mà không cần dùng DOM.
Ưu điểm
Hãy xem xét nó có nghĩa gì với thẻ đơn giản <Button>
1 | // Button.js |
=> Note: This is a stateless (“dumb”) component
Điều đó có nghĩa 1 component khác sử dụng nó sẽ có dạng:
1 | // HomePage.js |
=> Note: This is a stateful (“smart”) component!
Khi render thông thường với hàm chuẩn ReactDOM.render
, đây sẽ là HTML output (Nhận xét được thêm song song để so sánh các cấu trúc trong HTML từ nguồn JSX)
1 | <button> <!-- <Button> --> |
Trái lại, khi render với shallow render
, chúng ta sẽ nhận chuỗi string với đoạn HTML như sau:
1 | <button> <!-- <Button> --> |
Nếu chúng ta test Button
với render thông thường, khi có 1 vấn đề với CheckmarkIcon
sau đó test với Button
sẽ fail. Điều này rất khó khăn để tìm nguyên nhân (không biết CheckmarkIcon
lỗi hay Button
lỗi). Sử dụng shallow renderder
, chúng ta cô lập được các sự cố do chúng ta không render 1 vài thẻ nào khác ngoài thẻ chúng ta đang test. Đây có thể coi là ưu điểm duy nhất của kỹ thuật này so với các kỹ thuật test component khác.
Nhược điểm
Vấn đề của shallow renderer là tất cả các assertions phải làm thủ công, và bạn có thể không bất cứ điều gì cần DOM.
react-testing-library
Để viết được nhiều test có thể maintain cái mà sẽ giống hơn cách mà chúng ta sử dụng component ở thực tế, chúng có có cái gọi là react-testing-library
. Thư viện này renders component của chúng ta với DOM thực tế và cung cấp các tiện ích để truy vấn nó.
Hãy xem xét component <Button />
. Đầu tiên, hãy check xem nó có được renders với con của nó, nếu có, và thứ 2 là nó có xử lý sự kiện click?
1 | import React from 'react'; |
Snapshot testing
Hãy bắt đầu bằng cách chắc chắn rằng nó render ra các component và không thay đổi nào được xảy ra kể từ lần cuối, lần mà nó được test thành công.
Chúng ta sẽ làm vậy bằng cách render nó và tạo 1 snapshot cái mà có thể so sánh với snapshot đã được commit trước đó. Nếu không có snapshot nào tồn tại, 1 cái mới sẽ được tạo.
Do đó, đầu tiên chúng ta sẽ call render. Nó sẽ render component <Button />
vào container, và mặc định là 1 <div>
được thêm vào document.body
. Chúng ta sau đó sẽ tạo ra 1 snapshot và expect snapshot đó giống với snapshot đã tồn tại (nhận vào từ lần chạy trước đó của test này) và commit nó lên repo.
1 | it('renders and matches the snapshot', () => { |
render
trả về 1 đối tượng với thuộc tính container cái sẽ chứa được render từ <Button />
component
Vì đây là render với DOM thông thường, chúng ta có thể truy vấn vào component bằng cách sử dụng container.firseChild
. Cái này sẽ là snapshot, được chứa trong thư mục __snapshots__
trong folder tests. Chắc chắn rằng bạn đã commit snapshots này lên repo của bạn.
Tuyệt vời. Bây giờ nếu bạn thực hiện 1 vài thay đổi với component <Button />
, test của bạn sẽ trả về fail và bạn sẽ nhận được thông báo cái gì đã thay đổi. Nếu thay đổi đó đúng với cái mà bạn muốn, hãy update snapshots và commit nó vào repo thôi :D.
Test các hành vi
Test các sự kiện như click vào button.
Chúng ta sẽ sử dụng mock function để làm việc này. Một mock function là 1 hàm sẽ theo dõi: nếu, sự thường xuyên và các đối số nào sẽ được gọi (if, how often and with what arguments
). Chúng ta truyền hàm này như là hàm xử lý sự kiện onClick, mô phỏng 1 lần nhấp và cuối cùng, kiểm tra hàm mock đã được gọi:
1 | it('handles clicks', () => { |
Và cuối cùng, test đầy đủ của chúng ta sẽ có dạng:
1 | import React from 'react'; |
Tổng kết
- Khi test action và reducer: sử dụng Unit Testing
- Khi test component: sử dụng snapshot
- Khi test saga: sử dụng snapshot, có thể test full saga hoặc step-by-step
Tài liệu tham khảo
React with redux-saga Testing
http://yoursite.com/2019/09/02/React-with-redux-saga-Testing/