Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the point of unit testing redux-saga watchers?

In order to get 100% coverage of my Saga files I'm looking into how to test watchers.

I've been googling around, there are several answers as to HOW to test watchers. That is, saga's that do a takeEvery or takeLatest.

However, all methods of testing seem to basically copy the implementation. So what's the point of writing a test if it's the same?

Example:

// saga.js

import { delay } from 'redux-saga'
import { takeEvery, call, put } from 'redux-saga/effects'
import { FETCH_RESULTS, FETCH_COMPLETE } from './actions'

import mockResults from './tests/results.mock'

export function* fetchResults () {
  yield call(delay, 1000)
  yield put({ type: FETCH_COMPLETE, mockResults })
}

export function* watchFetchResults () {
  yield takeEvery(FETCH_RESULTS, fetchResults)
}

Test method 1:

import { takeEvery } from 'redux-saga/effects'
import { watchFetchResults, fetchResults } from '../sagas'
import { FETCH_RESULTS } from '../actions'

describe('watchFetchResults()', () => {
    const gen = watchFetchResults()
    // exactly the same as implementation
    const expected = takeEvery(FETCH_RESULTS, fetchResults)
    const actual = gen.next().value

    it('Should fire on FETCH_RESULTS', () => {
      expect(actual).toEqual(expected)
    })
  })

Test method 2: with a helper, like Redux Saga Test Plan
It's a different way of writing, but again we do basically the same as the implementation.

import testSaga from 'redux-saga-test-plan'
import { watchFetchResults, fetchResults } from '../sagas'
import { FETCH_RESULTS } from '../actions'

it('fire on FETCH_RESULTS', () => {
  testSaga(watchFetchResults)
    .next()
    .takeEvery(FETCH_RESULTS, fetchResults)
    .finish()
    .isDone()
})

Instead I'd like to simply know if watchFestchResults takes every FETCH_RESULTS. Or even only if it fires takeEvery(). No matter how it follows up.

Or is this really the way to do it?

like image 477
publicJorn Avatar asked Mar 06 '17 11:03

publicJorn


2 Answers

I agree with John Meyer's answer that this is better suited for the integration test than for the unit test. This issue is the most popular in GitHub based on up votes. I would recommend reading it.

One of the suggestions is to use redux-saga-tester package created by opener of the issue. It helps to create initial state, start saga helpers (takeEvery, takeLatest), dispatch actions that saga is listening on, observe the state, retrieve a history of actions and listen for specific actions to occur.

I am using it with axios-mock-adapter, but there are several examples in the codebase using nock.

Saga

import { takeLatest, call, put } from 'redux-saga/effects';
import { actions, types } from 'modules/review/reducer';
import * as api from 'api';

export function* requestReviews({ locale }) {
  const uri = `/reviews?filter[where][locale]=${locale}`;
  const response = yield call(api.get, uri);
  yield put(actions.receiveReviews(locale, response.data[0].services));
}

// Saga Helper
export default function* watchRequestReviews() {
  yield takeLatest(types.REVIEWS_REQUEST, requestReviews);
}

Test example using Jest

import { takeLatest } from 'redux-saga/effects';
import { types } from 'modules/review/reducer';
import SagaTester from 'redux-saga-tester';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';

import watchRequestReviews, { requestReviews } from '../reviews';

const mockAxios = new MockAdapter(axios);

describe('(Saga) Reviews', () => {
  afterEach(() => {
    mockAxios.reset();
  });

  it('should received reviews', async () => {
    const services = [
      {
        title: 'Title',
        description: 'Description',
      },
    ];
    const responseData = [{
      id: '595bdb2204b1aa3a7b737165',
      services,
    }];

    mockAxios.onGet('/api/reviews?filter[where][locale]=en').reply(200, responseData);

    // Start up the saga tester
    const sagaTester = new SagaTester({ initialState: { reviews: [] } });

    sagaTester.start(watchRequestReviews);

    // Dispatch the event to start the saga
    sagaTester.dispatch({ type: types.REVIEWS_REQUEST, locale: 'en' });

    // Hook into the success action
    await sagaTester.waitFor(types.REVIEWS_RECEIVE);

    expect(sagaTester.getLatestCalledAction()).toEqual({
      type: types.REVIEWS_RECEIVE,
      payload: { en: services },
    });
  });
});
like image 61
Black Avatar answered Sep 16 '22 14:09

Black


It sounds like the point of testing them is to achieve 100% test coverage.

There are some things that you can unit test, but it is questionable if you should.

It seems to me that this situation might be a better candidate for an 'integration' test. Something that does not test simply a single method, but how several methods work together as a whole. Perhaps you could call an action that fires a reducer that uses your saga, then check the store for the resulting change? This would be far more meaningful than testing the saga alone.

like image 26
John Meyer Avatar answered Sep 17 '22 14:09

John Meyer