- 原文地址:Genuine guide to testing React & Redux applications
- 原文做者:Jakub Żmuda
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:jonjia
- 校對者:zephyrJS goldEli
前端只是一層薄薄的靜態頁面的時代已經一去不復返了。現代 web 應用程序變得愈來愈複雜,邏輯也持續從後端向前端轉移。然而,當涉及到測試時,許多人都保持着過期的心態。若是你使用的是 React 和 Redux,可是因爲某些緣由對測試你的代碼不感興趣,我將在這裏向你展現如何以及爲何咱們天天都這樣作。前端
注意:我將使用 Jest 和 Enzyme。它們是測試 React & Redux 應用最流行的工具。我猜你已經用過或者能熟練使用它們了。react
React & Redux 應用構建在三個基本的構建塊上:actions、reducers 和 components。是獨立測試它們(單元測試),仍是一塊兒測試(集成測試)取決於你。集成測試會覆蓋到整個功能,能夠把它想成一個黑盒子,而單元測試專一於特定的構建塊。從個人經驗來看,集成測試很是適用於容易增加但相對簡單的應用。另外一方面,單元測試更適用於邏輯複雜的應用。儘管大多數應用都適合第一種狀況,但我將從單元測試開始更好地解釋應用層。android
這裏有一個可用的 應用。當你第一次進入頁面的時候,不會顯示圖片。你能夠經過點擊按鈕來獲取一張圖片。我使用了免費的 Dog API。如今讓咱們寫一些測試。能夠查看個人 源碼。ios
爲了展現一隻狗的圖片,咱們首先要獲取它,若是你不熟悉 thunk,別擔憂。Thunk 是一箇中間件,它能夠給咱們返回一個函數,而不是 action 對象。咱們能夠用它根據 HTTP 請求結果來 dispatch 對應的成功的 action 或者失敗的 action。git
咱們要測試從 API 成功取回的數據是否 dispatch 了成功的 action,而且將數據一塊兒傳遞。爲了作到這一點,咱們將使用 redux-mock-store。github
注意:我使用 axios 來做爲客戶端請求工具,用 axios-mock-adapter 來 mock 實際 API 的請求。你能夠自由選擇適合你的工具。web
import configureMockStore from 'redux-mock-store';
import { FETCH_DOG_REQUEST, FETCH_DOG_SUCCESS } from '../../constants/actionTypes';
import fetchDog from './fetchDog';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
describe('fetchDog action', () => {
let store;
let httpMock;
const flushAllPromises = () => new Promise(resolve => setImmediate(resolve));
beforeEach(() => {
httpMock = new MockAdapter(axios);
const mockStore = configureMockStore();
store = mockStore({});
});
it('fetches a dog', async () => {
// given
httpMock.onGet('https://dog.ceo/api/breeds/image/random').reply(200, {
status: 'success',
message: 'https://dog.ceo/api/img/someDog.jpg',
});
// when
fetchDog()(store.dispatch);
await flushAllPromises();
// then
expect(store.getActions()).toEqual(
[
{ type: FETCH_DOG_REQUEST },
{ payload: { url: 'https://dog.ceo/api/img/someDog.jpg' }, type: FETCH_DOG_SUCCESS }
]);
})
});
複製代碼
一開始,讓咱們在 beforeEach() 中進行 mock store 和模擬的 http 客戶端的初始化。在測試中,咱們爲請求指定結果。以後,執行咱們的 action 建立函數。由於咱們使用了 thunk,所以它會返回一個函數,咱們把 store 的 dispatch 方法傳給這個函數。在進行任何斷言以前,請求須要變爲 resolved,所以咱們要確保沒有 pending 的 Promise。redux
const flushAllPromises = () => new Promise(resolve => setImmediate(resolve));
複製代碼
這行代碼會把全部的 promise 放到一個單獨的事件循環中。window.setImmediate 是用來在瀏覽器已經完成了好比事件和顯示更新等其餘操做後,結束這些長時間運行的操做,並當即執行它的回調函數。 在這個例子中,掛起的 HTTP 請求就是咱們要完成的操做。此外,因爲這不是一個標準的瀏覽器特性,因此你不該該在正式代碼中使用它。axios
我認爲 reducers 是應用程序的核心。若是你開發功能豐富、複雜的系統,這部分就會變得很複雜。若是你引入了一個 bug,之後可能很難查找。這就是爲何測試 reducers 很是重要。咱們正在構建的應用很是簡單,但我但願你能獲取到圖片。後端
每一個 reducer 都會在應用啓動時被調用,所以須要一個初始狀態。聽任你的初始狀態爲 undefined 會讓你在組件中寫好多校驗代碼。
it('returns initial state', () => {
expect(dogReducer(undefined, {})).toEqual({url: ''});
});
複製代碼
這段代碼很直接,咱們使用 undefined 的狀態運行 reducer,並檢查它是否會返回帶有初始值的狀態。
咱們還必須保證那個 reducer 能正確的響應成功的請求,並獲取到圖片的 URL。
it('sets up fetched dog url', () => {
// given
const beforeState = {url: ''};
const action = {type: FETCH_DOG_SUCCESS, payload: {url: 'https://dog.ceo/api/img/someDog.jpg'}};
// when
const afterState = dogReducer(beforeState, action);
// then
expect(afterState).toEqual({url: 'https://dog.ceo/api/img/someDog.jpg'});
});
複製代碼
Reducers 應該是純函數,沒有反作用。這會讓測試它們變得很是簡單。提供一個以前的狀態,觸發一個 action,而後驗證輸出狀態是否正確。
在咱們開始以前,讓咱們先談談組件有哪些方面值得測試。咱們顯然沒法測試組件是否好看。可是,咱們絕對應該測試某些條件性的元素是否能成功顯示;或者對組件執行某些操做(不是 redux 中的 action),經過組件 props 傳遞的方法是否會被調用。
在咱們的系統中,咱們徹底依賴 redux 管理應用的狀態,所以咱們全部的組件都是無狀態的。
注意:若是你在尋找優雅的 Enzyme 斷言庫,能夠查看 enzyme-matchers
組件的結構很簡單。咱們有 DogApp 根組件和用來獲取並顯示狗的圖片的 RandomDog 組件。 RandomDog 組件的 props 以下:
static propTypes = {
dogUrl: PropTypes.string,
fetchDog: PropTypes.func,
};
複製代碼
Enzymes 可讓咱們用兩種方式來渲染一個組件。Shallow Rendering 意味着只有根組件會被渲染。若是你把 shallow rendered 組件的文本打印出來,你會發現全部子組件都沒有被渲染。Shallow rendering 很是適合單獨測試組件,而且從 Enzyme 3 開始(Enzyme 2 中也是可選的),它會調用生命週期的方法,好比 componentDidMount()。咱們稍後再介紹第二種方法。
如今咱們來寫 RandomDog 組件的測試用例。
首先,咱們要確保沒有圖片 URL 時,要顯示佔位符,並且不該該顯示圖片。
it('should render a placeholder', () => {
const wrapper = shallow(<RandomDog />);
expect(wrapper.find('.dog-placeholder').exists()).toBe(true);
expect(wrapper.find('.dog-image').exists()).toBe(false);
});
複製代碼
其次,在提供圖片 URL 時,圖片應該替換佔位符顯示出來。
it('should render actual dog image', () => {
const wrapper = shallow(<RandomDog dogUrl="http://somedogurl.dog" />);
expect(wrapper.find('.dog-placeholder').exists()).toBe(false);
expect(wrapper.find('img[src="http://somedogurl.dog"]').exists()).toBe(true);
});
複製代碼
最後,點擊獲取狗的圖片按鈕,應該會執行 fetchDog() 方法。
it('should execute fetchDog', () => {
const fetchDog = jest.fn();
const wrapper = shallow(<RandomDog fetchDog={fetchDog}/>);
wrapper.find('.dog-button').simulate('click');
expect(fetchDog).toHaveBeenCalledTimes(1);
});
複製代碼
注意:在這個例子中,我使用了元素和類選擇器。若是你發現它很脆弱並重構了代碼,能夠考慮切換到 custom attributes。
我用一些陳詞濫調來講明單元測試的問題。
雖然單元測試是個很好的工具,但它並不能保證咱們正確鏈接了全部的組件,或者 reducer 訂閱了正確的 action。這是 bug 容易發生的位置,這就是爲何咱們須要集成測試。
是的,有些人認爲因爲上述緣由,單元測試是沒用的,但我認爲他們沒有面對過一個足夠複雜的系統來發現單元測試的價值。
咱們如今將它們捆綁在一塊兒並放在一個黑盒子中,而不是單獨和詳細地測試構建塊。咱們再也不關心內部是如何工做的,或是組件內部究竟發生了什麼。 這就是爲何集成測試很是有彈性和方便重構的緣由。你能夠切換整個底層機制而無需更新測試。
在集成測試中,咱們再也不須要 mock store。讓咱們使用真實的吧。
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import reducers from './reducers/index';
export default function setupStore(initialState) {
return createStore(reducers, {...initialState}, applyMiddleware(thunk));
}
複製代碼
就是這樣。如今,咱們有一個功能齊全的 store,是時候開始第一個測試了。咱們使用 Enzyme 的 mount 來(實現掛載類型的渲染)。Mount 很是適合集成測試,由於它會渲染整個底層組件樹。
正如咱們在單元測試中所作的那樣,咱們要檢查應用啓動時是否沒有顯示圖像。可是如今我沒有將空的圖像 URL 做爲組件的 prop 傳遞,而是將其包裝在 Provider 中,傳遞了咱們建立的 store。
it('should render a placeholder when no dog image is fetched', () => {
let wrapper = mount(<Provider store={store}><App /></Provider>);
expect(wrapper.find('div.dog-placeholder').text()).toEqual('No dog loaded yet. Get some!');
expect(wrapper.find('img.dog-image').exists()).toBe(false);
});
複製代碼
沒有什麼特別的是吧?咱們來看第二個測試用例。
it('should fetch and render a dog', async () => {
httpMock.onGet('https://dog.ceo/api/breeds/image/random').reply(200, {
status: 'success',
message: 'https://dog.ceo/api/img/someDog.jpg'
});
const wrapper = mount(<Provider store={store}><App /></Provider>);
wrapper.find('.dog-button').simulate('click');
await flushAllPromises();
wrapper.update();
expect(wrapper.find('img[src="https://dog.ceo/api/img/someDog.jpg"]').exists()).toBe(true);
});
複製代碼
很容易對吧?這個測試描述了咱們和組件之間的真實交互。它涵蓋了單元測試所作的每一個方面,甚至更多。如今咱們能夠說構建塊不只可以單獨運行,並且可以以正確的方式結合起來。
哦,若是你對 Enzyme 很熟悉,還想知道我爲何調用 wrapper.update(),這就是緣由。簡而言之:這是 Enzyme 3 的一個 bug。也許在你閱讀這篇文章時,它會被修復。
Jest 提供了一種確保代碼更改不會改變組件的 render()方法輸出的方法。雖然編寫快照測試很是簡單快捷,但它們並不具備描述性,也沒法經過測試驅動開發過程。我看到的惟一使用案例是,當你對其餘人的未經測試的遺留代碼進行一些更改時,你並不想整理這些代碼,更不但願由於修改它而受到指責。
只須要從集成測試開始。你極可能以爲不會在你的項目中實施一個單元測試。這意味着你的複雜性不會在構建塊之間劃分,這樣很是好。你會節省不少時間。另外一方面,有些系統會利用單元測試的能力。二者都有用武之地。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。