flummox icon indicating copy to clipboard operation
flummox copied to clipboard

Simulate actions when testing stores

Open emmenko opened this issue 9 years ago • 10 comments

Hi @acdlite (again) ;)

I'm sure I'm missing something but I can't get it to work. Can someone help me here?

This is my setup

class UserActions extends Actions {
  getUser() {
    return {
      name: 'John Doe',
      email: '[email protected]'
    }
  }
}

class UserStore extends Store {
  constructor(flux) {
    super()
    let userActions = flux.getActions('user')
    this.register(userActions.getUser, this.handleFetchedUser)

    this.state = {
      user: {name: '', email: ''}
    }
  }

  handleFetchedUser(user) {
    this.setState({
      user: user
    })
  }
}

class Dispatcher extends Flux {
  constructor() {
    super()
    this.createActions('user', UserActions)
    this.createStore('user', UserStore, this)
  }
}


describe('UserStore', () => {
  let store, dispatcher
  beforeEach(() => {
    dispatcher = new Dispatcher()
    store = new UserStore(dispatcher)
  })

  it('should update state with fetched user', () => {
    spyOn(store, 'handler').and.callThrough()
    spyOn(store, 'handleFetchedUser')
    let actions = dispatcher.getActions('user')
    FluxTestUtils.simulateAction(store, actions.getUser, 'foo')
    expect(store.handler).toHaveBeenCalled()
    expect(store.handleFetchedUser).toHaveBeenCalledWith('foo')
  })
})

I would expect handleFetchedUser to have been called but it's not

Expected spy handleFetchedUser to have been called.

Any idea? Thanks :)

emmenko avatar Mar 10 '15 21:03 emmenko

Hi again! Looks like you're testing the wrong store — the one in beforeEach() instead of the one inside your Flux class.

Try this:

it('should update state with fetched user', () => {
  spyOn(store, 'handler').and.callThrough()
  spyOn(store, 'handleFetchedUser')
  let actions = dispatcher.getActions('user')
  let store = dispatcher.getStore('user')
  FluxTestUtils.simulateAction(store, actions.getUser, 'foo')
  expect(store.handler).toHaveBeenCalled()
  expect(store.handleFetchedUser).toHaveBeenCalledWith('foo')
})

acdlite avatar Mar 10 '15 21:03 acdlite

Also, you don't need to use TestUtils in this case. You can just call the action like you normally would. simulateAction() is only needed if you want to test your stores in isolation, without instantiating a Flux or Actions class.

acdlite avatar Mar 10 '15 21:03 acdlite

Uh, fastest support ever! ;)

So yes, I would also like to test the Store in isolation but what I would like to achieve first is to test that my handleFetchedUser handler actually updates the state.

A couple of things though:

  • how can I test it in isolation without passing a Flux instance? Since in the constructor register is called, should I just pass a mock to new UserStore() ?
  • if I want to test that the state is updated when I dispatch an action, should I test with Flux and Actions or is it possible to do it in "isolation"? How would you do it?

PS: your suggestion didn't work :( (also store should be declared before spying on it)

it('should update state with fetched user', () => {
  let store = dispatcher.getStore('user')
  spyOn(store, 'handler').and.callThrough()
  spyOn(store, 'handleFetchedUser')
  let actions = dispatcher.getActions('user')
  FluxTestUtils.simulateAction(store, actions.getUser, 'foo')
  expect(store.handler).toHaveBeenCalled()
  expect(store.handleFetchedUser).toHaveBeenCalledWith('foo')
})

emmenko avatar Mar 10 '15 21:03 emmenko

Oh, and I wanted to use simulateAction because I didn't want to actually execute the action (imagine it has to fetch data from a server). Still not a good idea to use it?

emmenko avatar Mar 10 '15 21:03 emmenko

Yeah, testing in isolation with simulateAction() is the right approach, especially if it fetches data.

Hmm, not sure why it's not working after updating it like that... I'll look over it some more.

Yes, in order to use simulateAction() you'll have to pass a mock to UserStore, like so:

let mockDispatcher = {
  getActions() {
    return {
      user: 'userActionId', // this can be anything, as long as it matches below
    }
  }
};

let store = new Store(mockDispatcher);
TestUtils.simulateAction(store, 'userActionId', 'foo');

acdlite avatar Mar 10 '15 22:03 acdlite

Ok thanks, I'll have another look tomorrow as well. Maybe I'm missing something...

emmenko avatar Mar 10 '15 22:03 emmenko

With my stores I usually pass in the actions directly instead of the flux instance, simplifies testing a bit:

let sessionActions = keyMirror({login: null, logout: null, receiveSession: null});

...

it('handles login success payload', () => {
  let store = new SessionStore(sessionActions);
  FluxTestUtils.simulateActionAsync(store, sessionActions.login, 'success', { access_token: 'foo', user_id: 12 });

  expect(store.isLoading()).to.equal(false);
  expect(store.isLoggedIn()).to.equal(true);
  expect(store.getUserId()).to.equal(12);
  expect(store.getAccessToken()).to.equal('foo');
});

tappleby avatar Mar 10 '15 23:03 tappleby

@tappleby but how do you register your store to the dispatcher?

// suggested way
this.createActions('session', SessionActions)
this.createStore('session', SessionStore, this)

// with actions?
this.createActions('session', SessionActions)
this.createStore('session', SessionStore, this.getActions('session'))

emmenko avatar Mar 11 '15 09:03 emmenko

@acdlite I've made a PR with the failing test in case you want to have a look https://github.com/acdlite/flummox/pull/74

emmenko avatar Mar 11 '15 11:03 emmenko

Update:

So I was able to make this working

class UserStore extends Store {

  constructor(flux) {
    super()
    this.registerActions(flux)
    this.state = {
      user: {
        name: '',
        email: ''
      }
    }
  }

  registerActions(flux) {
    let userActions = flux.getActions('user')
    this.register(userActions.getUser, this.handleFetchedUser)
  }

  handleFetchedUser(user) {
    this.setState({
      user: user
    })
  }
}

it('should update state with fetched user', () => {
  let mockDispatcher = {
    getActions() {
      return {
        getUser: 'userActionId'
      }
    }
  }
  let userStore = new UserStore(mockDispatcher)
  spyOn(userStore, 'handleFetchedUser').and.callThrough()
  // we register our actions again, this time with the spy
  userStore.registerActions(mockDispatcher)

  FluxTestUtils.simulateAction(userStore, 'userActionId', 'foo')
  expect(userStore.handleFetchedUser).toHaveBeenCalledWith('foo')
  expect(userStore.getState()).toEqual({
    user: 'foo'
  })
})

Couple of things to note here:

  • it looks like the function that I spy on never gets called because it was already registered when the store was instantiated
  • by simply registering actions + handlers again (this time the function has a spy on it) it magically works (not sure why though...)

More weird things:

  • class methods that start with _ are not accessible...is that expected? I never had this problem with coffeescript...

This works

class UserStore extends Store {
  constructor(flux) {
    this.registerActions(flux) // ok
  }
  registerActions(flux){
    // do something
  }
}

let store = new UserStore({})
store.registerActions({}) // ok

This doesn't

class UserStore extends Store {
  constructor(flux) {
    this._registerActions(flux) // ok
  }
  _registerActions(flux){
    // do something
  }
}

let store = new UserStore({})
store._registerActions({}) // TypeError: undefined is not a function

WTF?!

emmenko avatar Mar 11 '15 12:03 emmenko