mercredi 22 février 2017

testing custom redux middleware, which handles ajax request

I am building a react-redux app, using custom redux middleware. In the definition of my project, action only provides an object to define action type and necessary parameters for middleware and reducer. All the ajax request will be handle by middleware. This is the life cycle would look like: action -> middleware(if action is intercepted) -> reducer -> store

When the user tries to log in, the operation on the react component will fire an action, which would look like this:

export function login(username, password) {
  return {
    type: 'LOGIN',
    username: username,
    password: password
  }
}

export function authSucceed(username, isAdmin) {
  return {
    type: 'AUTHSUCCEED',
    username: username,
    isAdmin: isAdmin
  }
}

export function authFail(text) {
  return {
    type: 'AUTHFAIL',
    errorMessage: text
  }
}

Then middleware will use the parameters passed in action to send ajax request, which would be like this.

export function customedMiddleware(store) {
  return next => action => {
    if (action.type === 'LOGIN') {
      axios.post(url + '/api/login', {
        username: action.username,
        password: action.password
      })
        .then(res => {
          if (res.status === 200) {
            store.dispatch(actions.authSucceed(res.data.username, res.data.isAdmin));
          } else {
            store.dispatch(actions.authFail(res.data));
          }
        })
        .catch(error => console.log(error));
      }
      return next(action);
    };
  }

After the middleware sends login request to server, depending on whether the authentication succeeds or not, the middleware will dispatch some action in reducer correspondingly. Since authSucceed and authFail would not be intercepted by middleware, reducer will process accordingly.

export default function(state = false, action) {
  switch(action.type) {
    case 'AUTHSUCCEED': 
      return true;
    case 'AUTHFAIL': 
        return false;
    case 'LOGOUT': 
        return false;
  }
  return state;
}

What has been done here in reducer is to change the system state. If the state is true, the front-end will render the information page. If the state is false, the front-end will remain in the login page.

I like system definition this way. Every MVC part is well isolated. However, it's very difficult to test the middleware. Currently, I am testing this way:

it('should dispatch authSucceed if signup with correct info', () => {
  nock('http://localhost:8080')
    .post('/api/signup', {
      username: 'bruce',
      password: 'Gx1234'
    })
    .reply(200, {
      username: 'bruce',
      isAdmin: false
    });

  const createStoreWithMiddleware = applyMiddleware(customedMiddleware)(createStore);
  const store = createStoreWithMiddleware(reducers);
  const dispatch = sinon.spy(store, 'dispatch');

  store.dispatch(actions.login('bruce', 'Gx1234'));
  setTimeout(() => {
    expect(dispatch.calledWith({
      type: 'AUTHSUCCEED',
      username: 'bruce',
      isAdmin: false
    })).to.be.true;
  }, 100);
});

I dispatch login action. Then spy the whether authSucceed action and authFail action will be called correctly within 100ms. This method works if there is only one test to be run. If there are more then one test running in sequence, they might affect each other. I have to adjust the time delay of the setTimeout to make it work for all cases, which is 10ms. I don't feel comfortable this way. I can't make sure whether it just work for me or for everybody too, since absolute time is related to hardware. I would really appreciate if anybody can give me some advice on how to test this custom middleware.

Aucun commentaire:

Enregistrer un commentaire