How can I test async axios requests in redux-saga?

3.7k Views Asked by At

I have a saga of the form:

export function* apiRequest(apiBaseUrl, action) {
  const axiosInst = getAxiosInst(apiBaseUrl);

  try {
    if (!action.serviceName) {
      throw new Error("No service name provided");
    }
    const response = yield call( axiosInst.get, `/${action.serviceName}/foo-api` );

    const data = response.data;

    let resultAction;

    switch (response.status) {
      case 404:
        resultAction = INVALID_ENTITY_REQUESTED;
        break;
      case 200:
        ...
      default:
        throw new Error("Invalid response from server.");
    }
    yield put({ type: resultAction, data });
  } catch (err) {
    yield put({
      type: ERROR,
      error: err.message || "There was an unknown error."
    });
  }
}

export function* watchApiRequest(apiBaseUrl) {
  const boundApiRequest = apiRequest.bind(null, apiBaseUrl);

  yield takeEvery(API_CALL, boundApiRequest);
}

And a test like the following:

import { apiRequest } from "../services/apiRequest.js";
import MockAdapter from "axios-mock-adapter";
import { default as axios } from "axios";
import { put } from "redux-saga/effects";
import {
  API_CALL,
  API_SUCCESS
} from "../common/actions.js";

describe("Saga that will run on every api call event", () => {
  const mock = new MockAdapter(axios);

  afterEach(() => {
    mock.reset();
  });

  it("should dispatch the correct event when an API request succeeds", () => {
    mock.onGet().reply(200, { foo: "bar" });

    const generator = apiRequest("", {
      type: API_CALL,
      serviceName: "test"
    });

    generator.next();

    expect(generator.next().value).toMatchObject(
      put({
        type: API_SUCCESS,
        data: { foo: "bar" }
      })
    );
  });
});

This doesn't work. My test fails with a result like the following:

Expected value to match object:
      {"@@redux-saga/IO": true, "PUT": {"action": {"type": "API_SUCCESS"}, "channel": null}}
    Received:
      {"@@redux-saga/IO": true, "PUT": {"action": {"error": "Cannot read property 'data' of undefined", "type": "ERROR"}, "channel": null}}

The code appears to work fine in practical usage, but when I try to test it in this way, it seems that the promise from the async call to the API via Axios doesn't resolve. I've searched around for some guidance on testing API calls in Axios like this, and seen some suggestions that instead of mocking the API response with Axios mock adapter, I should feed the generator function the response by calling generator.next({status: 200, data: { foo: "bar" }) before the expect(...) clause, but this doesn't seem to work either.

I find the redux-saga docs on testing a little opaque, what am I doing wrong?

1

There are 1 best solutions below

2
On

I figured out the answer. It's about the nature of generator functions.

generator.next() advances the generator to its next yield instruction; for the purposes of testing like this, mocking responses to your API requests is actually not required, you can just inject the responses into the generator by passing the desired value to the generator's next() method.

So, in my test code above, there are two calls to next(). The first call to next() will result in the saga making its API request. The second will advance to the next yield, either dispatching the action with put() or dispatching an error in the catch block. The trick is that the desired mock API response should be provided in the second call to next()