简体   繁体   中英

Testing non-promises after promise completes with Mocha, Chai and Sinon

I've created a small learning project in Redux, and have essentially copied the middleware for asynchronous actions from the documentation . Now I want to write some tests for it, to make sure that it will work fine after I change around some things.

To write my tests, I'm using Mocha, with chai, chai-as-promised and sinon.

I have no problems with redux in any way, but I'm unsure of how to test things.

The relevant code from the middleware:

export default function callApiMiddleware({ dispatch }) {
  return next => action => {
    // ... various checks

    const [ requestType, successType, errorType ] = types;
    dispatch({ ...payload, type: requestType });

    return callApi().then(
      response => dispatch({ ...payload, type: successType, response }),
      error => dispatch({ ...payload, type: errorType, error })
    );
  }
}

The middleware dispatches the appropriate action whether the promise is fulfilled or rejected.

To test this, I'm stubbing out the dispatch function, along with others, and I also pass in a dummy promise, that I fulfill or reject depending on what I want to test.

A test I have written looks like this:

it('should trigger success action when successful', () => {
  let promise = callApiMiddleware({ dispatch, getState })(next)({
    ...action,
    callApi: () => new Promise((resolve, reject) => resolve('SUCCESS'))
  });
  return expect(promise).to.eventually.be.fulfilled;
});

This works fine, but the first problem I ran into was when I tried to simulate a rejected promise that could result from no internet connection, so I wrote this test:

it('should trigger failure action when failure occurs', () => {
  let promise = callApiMiddleware({ dispatch, getState })(next)({
    ...action,
    callApi: () => new Promise((resolve, reject) => reject('ERROR'))
  });
  return expect(promise).to.eventually.be.rejected;
});

But this fails with the following message:

AssertionError: expected promise to be rejected but it was fulfilled with undefined

Expected :[undefined]

Actual :[undefined]

This makes no sense to me, since I clearly pass in a promise that's only function is to be rejected.

My other problem is that I also want to make other assertions, that don't necessarly have anything to do with the promise itself, but have to be evaluated when the promise finishes eg I want to assert that the dispatch method was called twice. The dispatch method itself is stubbed out in the test using sinon in order to perform the desired assertions. Potentially, I want to make multiple assertions in this way.

I have tried the following:

it('should trigger success action when successful', () => {
  let promise = callApiMiddleware({ dispatch, getState })(next)({
    ...action,
    callApi: () => new Promise((resolve, reject) => resolve('SUCCESS'))
  });
  return Q.all([
    expect(promise).to.eventually.be.fulfilled,
    expect(dispatch).to.eventually.be.calledTwice
  ]);
});

But this returns some very large error that simply tells me that dispatch is not thenable ie not a promise.

I am out of ideas on how to do this, so any input would greatly be appreciated.

Promises do not rethrow errors, so if you catch an error in a first catch handler, promise will be fulfilled in next handler unless you throw the catched error again.

// this does not work
promise.catch((e) => console.log(e)).catch((e) => console.log(e));

If you want this to work, you have to rethrow an error.

promise.catch((e) => {
   console.log(e);
   throw e;
}).catch((e) => console.log(e));

If you want test to pass, you need to rethrow error catched in catch handler of a promise. So your code should look like this:

export default function callApiMiddleware({ dispatch }) {
  return next => action => {
    // ... various checks

    const [ requestType, successType, errorType ] = types;
    dispatch({ ...payload, type: requestType });

    return callApi().then(
      response => dispatch({ ...payload, type: successType, response }),
      error => {
          dispatch({ ...payload, type: errorType, error });
          throw error;
      }
    );
  }
}

The reason you get a Promise fulfilled with undefined is because this is what the middleware returns:

return callApi().then(
  response => dispatch({ ...payload, type: successType, response }),
  error => dispatch({ ...payload, type: errorType, error })
);

Since it doesn't rethrow the error in the second callback, the resulting Promise is fulfilled. Since it doesn't return anything either, it is fulfilled with undefined .

You can change the code to rethrow the error in this case:

return callApi().then(
  response => dispatch({ ...payload, type: successType, response }),
  error => {
    dispatch({ ...payload, type: errorType, error })
    throw error;
  }
);

This will give the result you expect but will report an unhandled rejection in the DevTools every time your request fails. Which may be fine in your case.

As for your second example:

But this returns some very large error that simply tells me that dispatch is not thenable ie not a promise.

It looks like .to.eventually.* works on Promises . Indeed, dispatch is not a Promise, so you can't use it like this. You might want to write something like this instead:

return expect(promise).to.eventually.be.fulfilled.then(() => {
  expect(dispatch).to.be.calledTwice();
});

Finally, I would encourage you to check out Redux Saga . Describing side effects with generators can be easier than with a custom middleware, and generators are way easier to test .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM