flummox
flummox copied to clipboard
Simulate actions when testing stores
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 :)
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')
})
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.
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 tonew 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')
})
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?
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');
Ok thanks, I'll have another look tomorrow as well. Maybe I'm missing something...
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 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'))
@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
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?!