jeffbski / redux-logic-test

redux-logic test utilities to facilitate the testing of logic. Create mock store
MIT License
37 stars 3 forks source link

How to test my logic with a try catch block #3

Open kopax opened 7 years ago

kopax commented 7 years ago

Hi @jeffbski, I finally got time to try this lib, sorry for taking so long!

I want to test the onErrorLoginRequest in a try catch block.

I have the following logic:

export const getAuthorizeLogic = createLogic({
  type: SUBMIT_LOGIN_REQUEST, // trigger on this action
  cancelType: LOCATION_CHANGE, // cancel if route changes
  latest: true, // use response for the latest request when multiple
  async process({ authService, forwardTo, pages, action }, dispatch, done) {
    const { username, password } = action.data;
    try {
      const jwt = await performLogin(authService, username, password);
      dispatch(onSuccessLoginRequest());
      dispatch(jwtLoaded(jwt));
      forwardTo(pages.pageDashboard.path); // Go to dashboard page
    } catch (err) {
      dispatch(onErrorLoginRequest(err));
      forwardTo(pages.pageLogin.path); // Go to dashboard page
    }
    done();
  },
});

Test:


describe('getAuthorizeLogic', () => {
  let store;
  beforeAll(() => {
    store = createMockStore({
      initialState: loginState,
      reducer: loginReducer, // default: identity reducer
      logic: getAuthorizeLogic, // default: []
      injectedDeps: {}, // default {}
      middleware: [] // optionalArr, other mw, exclude logicMiddleware
    });
  });

  describe('getAuthorizeLogic', () => {
    it('should make request and dispatch', () => {
      store.dispatch(onSuccessLoginRequest()) // use as necessary for your test
      store.dispatch(jwtLoaded({ toto: true }));
      store.whenComplete(() => expect(store.actions).toEqual([
        { type: ON_SUCCESS_LOGIN_REQUEST },
        {
          type: LOAD_JWT_SUCCESS,
          jwt: { toto: true },
        },
      ]));
    });
  });
});

Work fine so far.

  1. I am trying to test my catch block now, is there a way to do so?
jeffbski commented 7 years ago

What I typically do is to create an object that has all my API calls in it and pass that in as an injected dep. Then you can easily pass in a mocked call when testing and can make it succeed or throw errors as you want.

So your code would look something like this:

// in your configure store
const api = {
  performLogin
};
const logicMiddleware = createLogicMiddleware(logic, { api }); // inject api as a dependency

export const getAuthorizeLogic = createLogic({
  type: SUBMIT_LOGIN_REQUEST, // trigger on this action
  cancelType: LOCATION_CHANGE, // cancel if route changes
  latest: true, // use response for the latest request when multiple
  async process({ api, authService, forwardTo, pages, action }, dispatch, done) {
    const { username, password } = action.data;
    try {
      const jwt = await api.performLogin(authService, username, password); 
      dispatch(onSuccessLoginRequest());
      dispatch(jwtLoaded(jwt));
      forwardTo(pages.pageDashboard.path); // Go to dashboard page
    } catch (err) {
      dispatch(onErrorLoginRequest(err));
      forwardTo(pages.pageLogin.path); // Go to dashboard page
    }
    done();
  },
});

That way when we are testing, you can inject whatever you want for api.performLogin, even changing it for each test, just set the injectedDeps in your createMockStore. So you can test any type of success or failure scenario.

kopax commented 7 years ago

That way when we are testing, you can inject whatever you want

I have tried an apparently my code is failing right after I try to read from injectedDeps.

image

Or

image

I'd like to take a particular attention for that line

I was never using done before and never felt that it was required for my test.

I've try to add it on a test logic and I have that kind of error with the done:

dka@dev-01:[/workspace/wip/myappurl/backoffice-logic]: jest /workspace/wip/myappurl/backoffice-logic/app/containers/App/logics/tests/getLogoutLogic.test.js
(node:14883) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: expect(jest.fn()).toHaveBeenCalledWith(expected)

Expected mock function to have been called with:
  ["/"]
But it was not called.
(node:14883) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
 FAIL  app/containers/App/logics/tests/getLogoutLogic.test.js (6.322s)
  ● getLogoutLogic › getLogoutLogic › should make logout request and dispatch

    Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

      at Timeout.callback [as _onTimeout] (node_modules/jsdom/lib/jsdom/browser/Window.js:480:19)
      at ontimeout (timers.js:365:14)
      at tryOnTimeout (timers.js:237:5)
      at Timer.listOnTimeout (timers.js:207:5)

  getLogoutLogic
    getLogoutLogic
      ✕ should make logout request and dispatch (5004ms)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        7.041s
Ran all test suites matching "/workspace/wip/myappurl/backoffice-logic/app/containers/App/logics/tests/getLogoutLogic.test.js".

while I only have warning without it :

dka@dev-01:[/workspace/wip/myappurl/backoffice-logic]: jest /workspace/wip/myappurl/backoffice-logic/app/containers/App/logics/tests/getLogoutLogic.test.js
 PASS  app/containers/App/logics/tests/getLogoutLogic.test.js
  getLogoutLogic
    getLogoutLogic
      ✓ should make logout request and dispatch (4ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.135s, estimated 7s
Ran all test suites matching "/workspace/wip/myappurl/backoffice-logic/app/containers/App/logics/tests/getLogoutLogic.test.js".
(node:15007) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: expect(jest.fn()).toHaveBeenCalledWith(expected)

Expected mock function to have been called with:
  ["/"]
But it was not called.
(node:15007) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

This is what my async services for the most look like:

const managers = {
  getAll() {
    const options = {
      method: 'GET',
    };
    return api('http://myserver:31735/v1/managers', options)
      .then((response) => Promise.resolve(response));
  },
};
export default managers;
  1. How are you supposed to pass actions ?
  2. I thought it was pretty standard to write service like this. Apparently I am not splitting the code in a correct way.
  3. Should we always use done()?
kopax commented 7 years ago

Any update on this?

jeffbski commented 7 years ago

@kopax sorry for the delay, I'll try to follow up on Monday.

jeffbski commented 7 years ago

Regarding your question about needing to use done in your tests, if you have an async test (meaning that you have to wait for somethings to happen like when using API's with promises) then you either need to return the promise to the test or add a done cb and call that. I recommend returning the promise as the better of the two ways to handle things since it will give you a better error message when things fail. I should probably update my example to show that as well, it would look like this:

    it('should fetch answer and dispatch', () => {
      store.dispatch({ type: 'FOO' }); // start fetching
      return store.whenComplete(() => { // all logic has completed, return promise
        expect(store.actions).toEqual([
          { type: 'FOO' },
          { type: 'FOO_SUCCESS', payload: 42 }
        ]);
      });
    });

Let me know if you still have any issues after you update your code to return the promise. It looks like it is rejecting so I imagine once you change this it will be more clear what the reject error actually is.

kopax commented 7 years ago

Hi @jeffbski, thanks again for your answer.

I have tried to return store.whenComplete(...) but it didn't change the result

image

I don't understand why it fails silently. I have my test that pass, but apparently the tools that inject dependency doesn't work for me.

I have tried to added a console.log in my mock of fn:follow fn:api, it never get called. Is it working for you ?

jeffbski commented 7 years ago

Note that when you return the promise for a test you should not use the done cb, you need to do one or the other, not both. So you should remove that from the signature of your it test

it('should fetch answer and dispatch', () => { // do not include done cb
      store.dispatch({ type: 'FOO' }); // start fetching
      return store.whenComplete(() => { // all logic has completed, return promise
        expect(store.actions).toEqual([
          { type: 'FOO' },
          { type: 'FOO_SUCCESS', payload: 42 }
        ]);
      });
    });

Hopefully this should help it start reporting the errors.

If not maybe post everything to a gist or a repo where I can take a deeper look.

kopax commented 7 years ago

I have tried and unfortunately, it didn't change.

I have just created a demo project so you can test yourself.

git clone git@github.com:kopax/redux-logic-example.git
cd redux-logic-example
npm install
npm test
google-chrome coverage/lcov-report/index.html # only if you have google-chrome installed
jeffbski commented 7 years ago

excellent. I'll spin it up on Monday and see if I can figure out what is causing the issue.

jeffbski commented 7 years ago

I installed from your repo and took a look at your coverage.

Basically the coverage is low for your logic.js since you didn't dispatch any actions that would cause that logic to run.

So in your logic.js, the logic action types it is listening for are LOAD_FROM_SERVER and ON_NAVIGATE.

In your logic.test.js, you are only dispatching ON_LIST_SUCCESS so neither of these logic would run for that action type and thus coverage is low.

Also note that you should probably just always reset (by recreating the store using createMockStore) the store for every iteration using beforeEach rather than beforeAll. You generally want clean state for each test, so that's what I would suggest. So change your beforeAll to beforeEach and then add some additional tests where you dispatch LOAD_FROM_SERVER and ON_NAVIGATE to have the tests exercise your logic.