差異處
這裏顯示兩個版本的差異處。
Both sides previous revision 前次修改 下次修改 | 前次修改 | ||
web:reactjs:test:teststaticmethod [2019/03/10 14:58] tony [Problem] |
web:reactjs:test:teststaticmethod [2023/06/25 09:48] (目前版本) |
||
---|---|---|---|
行 1: | 行 1: | ||
+ | {{tag>reactjs}} | ||
====== React - How to test component with static method? ====== | ====== React - How to test component with static method? ====== | ||
===== Problem ===== | ===== Problem ===== | ||
行 25: | 行 26: | ||
}; | }; | ||
</code> | </code> | ||
+ | 而Logout的元件中,則會透過Auth.signOut去執行登出的動作: | ||
+ | <code javascript> | ||
+ | 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; | ||
+ | </code> | ||
+ | 在先前針對[[web:reactjs:react_hook:usecontext|useContext]]的使用文章中,參考過別人使用jest mock static的寫法如下: | ||
+ | <code javascript> | ||
+ | const auth_do_nothing = jest.fn(); | ||
+ | auth_do_nothing.mockImplementation(()=>{}); | ||
+ | Auth.signOut = auth_do_nothing.bind(Auth); | ||
+ | </code> | ||
+ | 但這寫法存在會將狀態延續至下一個testcase中(testsuite似乎沒影響);因此本篇文章將分享透過[[https://sinonjs.org/releases/latest/|Sinon]]去mock static method並且清除mock狀態的做法。 | ||
===== How to? ===== | ===== How to? ===== | ||
+ | 以Logout為例,我想要模擬使用者點擊Logout並透過Auth.signOut執行登出動作,完整的測試程式碼如下: | ||
+ | <code javascript> | ||
+ | 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); | ||
+ | }); | ||
+ | }); | ||
+ | </code> | ||
+ | 在test beforeEach中,我透過sinon.stub去模擬Auth.signOut doNothing的動作,即不給予任何的動作;而在afterEach中,我可以透過stub產生的instance去做restore的動作,讓Auth.signOut回復到模擬之前: | ||
+ | <code javascript> | ||
+ | let stubSignOut; | ||
+ | beforeEach(()=>{ | ||
+ | stubSignOut = sinon.stub(Auth, 'signOut'); | ||
+ | }); | ||
+ | |||
+ | afterEach(()=>{ | ||
+ | stubSignOut.restore(); | ||
+ | }); | ||
+ | </code> | ||
+ | 如果要驗證stub method是否有被呼叫,可以使用下面的做法: | ||
+ | <code javascript> | ||
+ | expect(stubSignOut.called).toBe(true); | ||
+ | </code> | ||
+ | 假如我是在Login form中想要模擬登入失敗的情形呢? 這裡我一樣用signOut當範例,我可以在stubSignOut instance上指定要拋出怎樣的例外: | ||
+ | <code javascript> | ||
+ | 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); | ||
+ | }); | ||
+ | </code> | ||
+ | 如果是要用回傳值的方式,則可以透過以下方式: | ||
+ | <code javascript> | ||
+ | it('Test stub with return value', ()=> { | ||
+ | stubSignOut.returns(true); | ||
+ | expect(Auth.signOut()).toBe(true); | ||
+ | expect(stubSignOut.called).toBe(true); | ||
+ | }); | ||
+ | </code> | ||
+ | 假如我要模擬像signIn有帶參數的情況呢? 以login失敗為例,可以使用withArgs來達到這個需求: | ||
+ | <code javascript> | ||
+ | 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); | ||
+ | }); | ||
+ | </code> | ||
+ | 可以看到sinon stub可以滿足我們基本的需求,而且使用也相當容易。我唯一在意的是它stub方式,需要將method name傳遞進去;假如以後有rename的需求,這會不容易馬上察覺。針對這個問題,如果之後有看到其它方法,會再分享給各位。 | ||
+ | ===== Reference ===== | ||
+ | * [[https://sinonjs.org/releases/v7.2.7/stubs/|Stubs - Sinon.JS]] | ||
+ | * [[https://www.robinwieruch.de/react-fetching-data/|How to fetch data in React?]] | ||
+ | ===== ===== | ||
+ | ---- | ||
+ | \\ | ||
+ | ~~DISQUS~~ |