- Notifications
You must be signed in to change notification settings - Fork126
Test Redux Saga with an easy plan.
License
jfairbank/redux-saga-test-plan
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Redux Saga Test Plan makes testing sagas a breeze. Whether you need to testexact effects and their ordering or just test your sagaput's a specificaction at some point, Redux Saga Test Plan has you covered.
Redux Saga Test Plan aims to embrace both integration testing and unit testingapproaches to make testing your sagas easy.
Requires globalPromise to be available
One downside to unit testing sagas is that it couples your test to yourimplementation. Simple reordering of yielded effects in your saga could breakyour tests even if the functionality stays the same. If you're not concernedwith the order or exact effects your saga yields, then you can take anintegrative approach, testing the behavior of your saga when run by Redux Saga.Then, you can simply test that a particular effect was yielded during the sagarun. For this, use theexpectSaga test function.
Import theexpectSaga function and pass in your saga function as an argument.Any additional arguments toexpectSaga will become arguments to the sagafunction. The return value is a chainable API with assertions for the differenteffect creators available in Redux Saga.
In the example below, we test that theuserSaga successfullyputs aRECEIVE_USER action with thefakeUser as the payload. We callexpectSagawith theuserSaga and supply anapi object as an argument touserSaga. Weassert the expectedput effect via theput assertion method. Then, we callthedispatch method with aREQUEST_USER action that contains the user idpayload. Thedispatch method will supply actions totake effects. Finally,we start the test by calling therun method which returns aPromise. TestswithexpectSaga will always run asynchronously, so the returnedPromiseresolves when the saga finishes or whenexpectSaga forces a timeout. If you'reusing a test runner like Jest, you can return thePromise inside your Jesttest so Jest knows when the test is complete.
import{call,put,take}from'redux-saga/effects';import{expectSaga}from'redux-saga-test-plan';function*userSaga(api){constaction=yieldtake('REQUEST_USER');constuser=yieldcall(api.fetchUser,action.payload);yieldput({type:'RECEIVE_USER',payload:user});}it('just works!',()=>{constapi={fetchUser:id=>({ id,name:'Tucker'}),};returnexpectSaga(userSaga,api)// Assert that the `put` will eventually happen..put({type:'RECEIVE_USER',payload:{id:42,name:'Tucker'},})// Dispatch any actions that the saga will `take`..dispatch({type:'REQUEST_USER',payload:42})// Start the test. Returns a Promise..run();});
expectSaga runs your saga with Redux Saga, so it will try to resolve effectsjust like Redux Saga would in your application. This is great for integrationtesting, but sometimes it can be laborious to bootstrap your entire applicationfor tests or mock things like server APIs. In those cases, you can useproviders which are perfect for mocking values directly withexpectSaga.Providers are similar to middleware that allow you to intercept effects beforethey reach Redux Saga. You can choose to return a mock value instead of allowingRedux Saga to handle the effect, or you can pass on the effect to otherproviders or eventually Redux Saga.
expectSaga has two flavors of providers,static providers anddynamicproviders. Static providers are easier to compose and reuse, but dynamicproviders give you more flexibility with non-deterministic effects. Here is oneexample below using static providers. There are more examples of providersinthedocs.
import{call,put,take}from'redux-saga/effects';import{expectSaga}from'redux-saga-test-plan';import*asmatchersfrom'redux-saga-test-plan/matchers';import{throwError}from'redux-saga-test-plan/providers';importapifrom'my-api';function*userSaga(api){try{constaction=yieldtake('REQUEST_USER');constuser=yieldcall(api.fetchUser,action.payload);constpet=yieldcall(api.fetchPet,user.petId);yieldput({type:'RECEIVE_USER',payload:{ user, pet},});}catch(e){yieldput({type:'FAIL_USER',error:e});}}it('fetches the user',()=>{constfakeUser={name:'Jeremy',petId:20};constfakeDog={name:'Tucker'};returnexpectSaga(userSaga,api).provide([[call(api.fetchUser,42),fakeUser],[matchers.call.fn(api.fetchPet),fakeDog],]).put({type:'RECEIVE_USER',payload:{user:fakeUser,pet:fakeDog},}).dispatch({type:'REQUEST_USER',payload:42}).run();});it('handles errors',()=>{consterror=newError('error');returnexpectSaga(userSaga,api).provide([[matchers.call.fn(api.fetchUser),throwError(error)]]).put({type:'FAIL_USER', error}).dispatch({type:'REQUEST_USER',payload:42}).run();});
Notice we pass in an array of tuple pairs (or array pairs) that contain amatcher and a fake value. You can use the effect creators from Redux Saga ormatchers from theredux-saga-test-plan/matchers module to match effects. Thebonus of using Redux Saga Test Plan's matchers is that they offer specialpartial matchers likecall.fn which matches by the function without worryingabout the specificargs contained in the actualcall effect. Notice in thesecond test that we can also simulate errors with thethrowError function fromtheredux-saga-test-plan/providers module. This is perfect for simulatingserver problems.
One good use case for integration testing is testing your reducer too. You canhook up your reducer to your test by calling thewithReducer method with yourreducer function.
import{put}from'redux-saga/effects';import{expectSaga}from'redux-saga-test-plan';constinitialDog={name:'Tucker',age:11,};functionreducer(state=initialDog,action){if(action.type==='HAVE_BIRTHDAY'){return{ ...state,age:state.age+1,};}returnstate;}function*saga(){yieldput({type:'HAVE_BIRTHDAY'});}it('handles reducers and store state',()=>{returnexpectSaga(saga).withReducer(reducer).hasFinalState({name:'Tucker',age:12,// <-- age changes in store state}).run();});
If you want to ensure that your saga yields specific types of effects in aparticular order, then you can use thetestSaga function. Here's a simpleexample:
import{testSaga}from'redux-saga-test-plan';functionidentity(value){returnvalue;}function*mainSaga(x,y){constaction=yieldtake('HELLO');yieldput({type:'ADD',payload:x+y});yieldcall(identity,action);}constaction={type:'TEST'};it('works with unit tests',()=>{testSaga(mainSaga,40,2)// advance saga with `next()`.next()// assert that the saga yields `take` with `'HELLO'` as type.take('HELLO')// pass back in a value to a saga after it yields.next(action)// assert that the saga yields `put` with the expected action.put({type:'ADD',payload:42}).next()// assert that the saga yields a `call` to `identity` with// the `action` argument.call(identity,action).next()// assert that the saga is finished.isDone();});
To see large effect objects while Expected & Actual result comparison you'll need to extend inspect options. Example:
importutilfrom'util';importtestSagafrom'redux-saga-test-plan';import{testableSaga}from'../sagas';describe('Some sagas to test',()=>{util.inspect.defaultOptions.depth=null;it('testableSaga',()=>{testSaga(testableSaga).next().put({/* large object here */}).next().isDone();});});
yarn add redux-saga-test-plan --devnpm install --save-dev redux-saga-test-planAbout
Test Redux Saga with an easy plan.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.