Problem
在撰寫程式時,我會習慣將同性質的utility method放在同一個class中。以實作Login、Logout的功能來說,我就會建立一個class叫Auth,會包含以下methods:
import React, { Component } from 'react'; const session_token_key = 'session_token'; export default class Auth extends Component { static signIn(username, password){ // check credential sessionStorage.setItem(session_token_key, 'session_info'); } static signOut(){ sessionStorage.clear(); } static checkLogin(){ if( !sessionStorage.getItem(session_token_key) ) { throw new Error("no session info"); } } };
而Logout的元件中,則會透過Auth.signOut去執行登出的動作:
import React, { useContext } from 'react'; import Auth from './Auth' import { NavItem, NavLink} from 'reactstrap'; import { AuthContext } from './AuthContext'; function Logout(){ const [ authState, setAuthState ] = useContext(AuthContext); const signOut = ()=>{ Auth.signOut(); setAuthState(false); window.location.href = '/'; }; return ( <NavItem aria-label="logout-nav-item" className="d-md-down-none" onClick={signOut}> <NavLink href="#" ><i className="icon-logout"></i></NavLink> </NavItem> ); } export default Logout;
在先前針對useContext的使用文章中,參考過別人使用jest mock static的寫法如下:
const auth_do_nothing = jest.fn(); auth_do_nothing.mockImplementation(()=>{}); Auth.signOut = auth_do_nothing.bind(Auth);
但這寫法存在會將狀態延續至下一個testcase中(testsuite似乎沒影響);因此本篇文章將分享透過Sinon去mock static method並且清除mock狀態的做法。
How to?
以Logout為例,我想要模擬使用者點擊Logout並透過Auth.signOut執行登出動作,完整的測試程式碼如下:
import Logout from '../Logout' import React, { useState, useEffect } from 'react'; import { AuthContext } from '../AuthContext'; import { render, fireEvent } from 'react-testing-library'; import Auth from '../Auth'; import sinon from 'sinon'; let authState = true; function LogoutComp(){ const [ state, setState ] = useState(true); useEffect(()=>{ authState = state; }); return <AuthContext.Provider value={[state, setState]}><Logout/></AuthContext.Provider>; } describe('<Logout/>',()=>{ let stubSignOut; beforeEach(()=>{ stubSignOut = sinon.stub(Auth, 'signOut'); }); afterEach(()=>{ stubSignOut.restore(); }); it('Test logout', () => { // given const utils = render(<LogoutComp/>); const logout_nav_item = utils.getByLabelText('logout-nav-item'); // when fireEvent.click(logout_nav_item); // then expect(authState).toBe(false); expect(stubSignOut.called).toBe(true); }); });
在test beforeEach中,我透過sinon.stub去模擬Auth.signOut doNothing的動作,即不給予任何的動作;而在afterEach中,我可以透過stub產生的instance去做restore的動作,讓Auth.signOut回復到模擬之前:
let stubSignOut; beforeEach(()=>{ stubSignOut = sinon.stub(Auth, 'signOut'); }); afterEach(()=>{ stubSignOut.restore(); });
如果要驗證stub method是否有被呼叫,可以使用下面的做法:
expect(stubSignOut.called).toBe(true);
假如我是在Login form中想要模擬登入失敗的情形呢? 這裡我一樣用signOut當範例,我可以在stubSignOut instance上指定要拋出怎樣的例外:
it('Test stub with throw', ()=> { stubSignOut.throws("name", "message"); try { Auth.signOut(); fail("should be failed"); } catch(e) { expect(e.name).toBe("name"); expect(e.message).toBe("message"); } expect(stubSignOut.called).toBe(true); });
如果是要用回傳值的方式,則可以透過以下方式:
it('Test stub with return value', ()=> { stubSignOut.returns(true); expect(Auth.signOut()).toBe(true); expect(stubSignOut.called).toBe(true); });
假如我要模擬像signIn有帶參數的情況呢? 以login失敗為例,可以使用withArgs來達到這個需求:
it('Test stub with arguments', ()=> { stubSignIn.withArgs('root', '123456').throws("login failed", "wrong password"); try { Auth.signIn('root', '123456'); fail("should be failed"); } catch(e) { expect(e.name).toBe("login failed"); expect(e.message).toBe("wrong password"); } expect(stubSignIn.called).toBe(true); });
可以看到sinon stub可以滿足我們基本的需求,而且使用也相當容易。我唯一在意的是它stub方式,需要將method name傳遞進去;假如以後有rename的需求,這會不容易馬上察覺。針對這個問題,如果之後有看到其它方法,會再分享給各位。
留言
張貼留言