Unit Test 是自動化測試裡面最貼近程式的一種。意思是說,unit test 的測試對象通常是一個 class, 一個 function 或者一個 React component 等可以用 code 來切開的單位;用另一個角度來說,unit test 是給 developer 看的。
如果用比較高階的 acceptance test 來比較的話,acceptance test 就離程式碼本身比較遠一點,acceptance test 的測試對象可能是一個功能 (例如說 signup 的能功,從按按鈕到建立一個 user 等等)。也可以說,acceptance test 是寫給用嘴巴不會寫 code 的人看的。(像是 PM XD)
舉例來說,一個 unit test 可能是:
確保 questionReducer 在接到 QUESTION_LOADED 事件的時候,可以 return 一個新的 question 作為 state
而一個 acceptance test 可能是:
確保當 user 按下 question link 的時候,他會被帶到 question 的頁面並且看到 render 好的 question 內容
為什麼要寫 unit test?
在 unit test 裡面,我們的目的是要確保我們寫的 code 和我們預期的行為一樣。
當同一份 code 越長越大,越來越多人加入一起開發之後,我們幾乎不太可能再用純人工去檢查每一個 function, 或每個 class 都如我們預期一般的運作。這時候如果有一個自動的測試幫我們在每次改動的時候,重覆地去檢查並且立刻回報給我們知道,那就可以大大地降低我們 debug 的時間。這代表參與開發的每一個人,都可以很大膽地去改動任何東西,因為只要 test 都跑過了,那幾乎就代表其他的東西沒有被不小心改壞。
這意味著 developer 可以放心地在任何時間去 refactor 程式的架構,久而久之就會形成一個良性的循環,讓這份 code 變得越來越穩定,也因為架構變好了,要加新功能或修改也可以再更短的時間內完成。總之就是好處多多潮爽der。
importquestionReducerfrom'reducers/questions';import*asActionTypefrom'actions/questions';describe('Reducer::Question',function(){it('returns an empty array as default state',function(){// setup
letaction={type:'unknown'};// execute
letnewState=questionReducer(undefined,{type:'unknown'});// verify
expect(newState).to.deep.equal([]);});describe('on LOADED_QUESTIONS',function(){it('returns the `response` in given action',function(){// setup
letaction={type:ActionType.LOADED_QUESTIONS,response:{responseKey:'responseVal'}};// execute
letnewState=questionReducer(undefined,action);// verify
expect(newState).to.deep.equal(action.response);});});});
所以我們的 test code 就會長這樣 spec/middleware/api.test.js:
importnockfrom'nock';importapiMiddleware,{CALL_API}from'middleware/api';describe('Middleware::Api',function(){letstore,next;letaction;letsuccessType='ON_SUCCESS';leturl='http://the-url/path';beforeEach(function(){store={};next=sinon.stub();action={[CALL_API]:{method:'get',url,successType}};});describe('when action is without CALL_API',function(){it('passes the action to next middleware',function(){action={type:'not-CALL_API'};apiMiddleware(store)(next)(action);expect(next).to.have.been.calledWith(action);});});describe('when action is with `CALL_API`',function(){letnockScope;beforeEach(function(){nockScope=nock(`http://the-url`)
.get('/path');});afterEach(function(){nock.cleanAll();});it('sends request to `path` with query and body',function(){nockScope=nockScope.reply(200,{status:'ok'});apiMiddleware(store)(next)(action);nockScope.done();});it('resolves returned promise when response when success',function(){nockScope=nockScope.reply(200,{status:'ok'});letpromise=apiMiddleware(store)(next)(action);returnexpect(promise).to.be.fulfilled;});it('dispatch successType with response when success',function(done){nockScope=nockScope.reply(200,{status:'ok'});letpromise=apiMiddleware(store)(next)(action);promise.then(()=>{expect(next).to.have.been.calledWith({type:successType,response:{status:'ok'}});done();});});});});
最後是 dispatch successType with response when success 這邊有 async(非同步) 的行為。Mocha 最基本的行為是不會等 async 的 code 執行完的。也就是說,在 async 的 code 被執行到之前,mocha 的 test case (it())就會先結束掉。這樣我們就會無法在 async 的行為完成之後再驗証一些行為。這個部份 mocha 的做法是在 it 後面跟的 function 帶一個參數(done),而 mocha 會等到這個 done 在 test case 裡面被執行到之後才結束這個 test case。如此一來,我們就可以在 async 的動作執行完之後再呼叫 done,這樣就可以確保 mocha 會在我們預期的時間點才結束 test case 了。
component 會根據 props.questions render 出對應的 elements
component 會 render 出一個指向 / 的 Link
於是我們的測試會長成這樣: spec/containers/Question.test.js
importContainer,{Question}from'containers/Question';importReactfrom'react';importTestUtilsfrom'react-addons-test-utils';describe.only('Container::Question',function(){letprops;letLink;beforeEach(function(){props={loadQuestions:sinon.stub(),questions:[{id:1,content:'question content 1'},{id:2,content:'question content 1'}]};Link=React.createClass({render(){return(<div>MOCKCOMPONENTCLASS</div>)}});Container.__Rewire__('Link',Link);});it('renders questions according to `props.questions`',function(){letdoc=TestUtils.renderIntoDocument(<Question{...props}/>);letquestionElements=TestUtils.scryRenderedDOMComponentsWithTag(doc,'p');expect(questionElements.length).to.equal(props.questions.length);});it('renders a link back to `/`',function(){letdoc=TestUtils.renderIntoDocument(<Question{...props}/>);letlink=TestUtils.findRenderedComponentWithType(doc,Link);expect(link).not.to.be.undefined;expect(link.props.to).to.equal('/');});});
describe('Component SignupModal',function(){letrequireImage;beforeEach(function(){requireImage=function(){}SignupModal.__Rewire__('requireImage',requireImage);});it('can be rendered',function(){// now we can render here
});});