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

2

14

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?

Sepulchre answered 6/3, 2017 at 11:34 Comment(1)
Yeah, I'm baffled by this, too. The example tests are all comparing the results to saga effect creators. That's not what I care about. I care about whether they put the store in the right state. Testing by the examples seems extremely brittle and doesn't prove anything.Christianly
D
9

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.

Dryad answered 28/3, 2017 at 13:55 Comment(0)
D
4

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 },
    });
  });
});
Durand answered 16/7, 2017 at 14:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.