SPAなどは特にそうだと思うけど、fetch
を使って非同期にAPIを叩き、その結果に基いて部分的にビューを書き換える、というのはよく行われる。
そういう時に面倒なのがテスト。
fetch
はwindow
オブジェクトに入っているからテスト環境には存在しないし、そもそもAPIを叩こうにもクロスオリジンやら何やらで上手くいかないことが多い。
イベントが発火した際にAPIを叩きに行くか、APIからのレスポンスに基いてビューが書き換えられるか、ということをテストしたい場合、いちいちAPIと通信できるように環境を整えるのは手間が掛かり過ぎる。
こういうケースでは、fetch
をスタブにしてしまったほうがよい。
テスト対象
以下のReactのコンポーネントについて、テストを書いていく。
import React from 'react'; export default class ApiRequest extends React.Component { constructor() { super(); this.state = {result: 'ボタンを押してください。'}; this.callApi = this.callApi.bind(this); } callApi(statusCode) { fetch(`https://httpbin.org/status/${statusCode}`).then(res => { this.setState({ result: res.status >= 200 && res.status < 300 ? '成功' : '失敗', }); }); } render() { const {result} = this.state; return ( <div> <button id="200" onClick={() => this.callApi(200)}> 200を叩く </button> <button id="500" onClick={() => this.callApi(500)}> 500を叩く </button> <div id="result">{result}</div> </div> ); } }
ボタンを押すとAPIを叩きに行き、そのレスポンスに応じてthis.state
を変更し、ビューが書き換わる。
仕組みとしては、よくあるものだと思う。
fetch がないと言われる
まずはこんな感じで書いてみる。
import assert from 'assert'; import React from 'react'; import {shallow} from 'enzyme'; import ApiRequest from '../ApiRequest'; describe('ApiRequest', () => { let wrapper; beforeEach(() => { wrapper = shallow(<ApiRequest />); }); it('最初は「ボタンを押してください。」と表示されている。', () => { assert(wrapper.find('#result').text() === 'ボタンを押してください。'); }); it('200のボタンを押下すると「成功」と表示される。', () => { wrapper.find('#200').simulate('click'); assert(wrapper.find('#result').text() === '成功'); }); it('500のボタンを押下すると「失敗」と表示される。', () => { wrapper.find('#500').simulate('click'); assert(wrapper.find('#result').text() === '失敗'); }); });
当然、失敗する。
ApiRequest ✓ 最初は「ボタンを押してください。」と表示されている。 1) 200のボタンを押下すると「成功」と表示される。 2) 500のボタンを押下すると「失敗」と表示される。 1 passing (37ms) 2 failing 1) ApiRequest 200のボタンを押下すると「成功」と表示される。: ReferenceError: fetch is not defined 2) ApiRequest 500のボタンを押下すると「失敗」と表示される。: ReferenceError: fetch is not defined
なのでまず、グローバルオブジェクトにfetch
を作る。
これは、個別のテストファイルに書くのではなく、設定ファイルか何かに書くのが一般的だと思う。
global.fetch = () => {};
もちろんこれだけではテストは通らず、TypeError: Cannot read property 'then' of undefined
と言われる。
ここから、fetch
をスタブにしていく。
fetch をスタブにする
先程のテストコードを以下のように書き換えると、テストが動くようになる。
import assert from 'assert'; import React from 'react'; import {shallow} from 'enzyme'; import sinon from 'sinon'; import ApiRequest from '../ApiRequest'; describe('ApiRequest', () => { let wrapper; let stub; beforeEach(() => { wrapper = shallow(<ApiRequest />); stub = sinon.stub(global, 'fetch'); }); afterEach(() => { stub.restore(); }); it('最初は「ボタンを押してください。」と表示されている。', () => { assert(wrapper.find('#result').text() === 'ボタンを押してください。'); }); it('200のボタンを押下すると「成功」と表示される。', done => { stub.returns(Promise.resolve({status: 200})); wrapper.find('#200').simulate('click'); setTimeout(() => { assert(wrapper.find('#result').text() === '成功'); done(); }, 0); }); it('500のボタンを押下すると「失敗」と表示される。', done => { stub.returns(Promise.resolve({status: 500})); wrapper.find('#500').simulate('click'); setTimeout(() => { assert(wrapper.find('#result').text() === '失敗'); done(); }, 0); }); });
fetch
はPromiseベースなので、スタブにして、FulFilledなpromiseオブジェクトを返すようにすればいい。
Promiseを使っているため、done
を使わないと上手く動かないので注意が必要。
sinon
やスタブについてはこちらを参照。
done
を使って非同期テストについてはこちらを参照。