fluxxor
fluxxor copied to clipboard
How to test Fluxxor apps
I'm using React with Fluxxor mixins. What's the recommended way to test my React components that use Fluxxor?
Ideally, the components that touch flux should be minimal and only pass data/callbacks into more functionally pure components (these top-level, flux-referencing components are referred to as "Containers" in this talk). Past that, any component that uses FluxMixin
will take the flux instance from a prop named flux
if it exists, so you can pass a mock Flux
instance in (or a real one, if you wish) and assert that things render correctly under the circumstances you care about.
That's pretty high level, so if you have a more concrete example/question, let me know.
Here's some code more or less copied from the quick start example. I set up TodoItem components to render with style green if complete and red otherwise. I'm trying to use Jest to simulate a "click" event, which should make the TodoItem green. There's no clear way to unit test this child component. Since I'm using an instance of Fluxxor.Flux
w/ FluxMixin
, and the TodoItem gets its todo prop from its parent, this.getFlux().actions.toggleComplete(this.props.todo)
won't work in my test code. What are your thoughts?
jest.dontMock '../todo-item'
jest.dontMock '../../fluxxors/todo-fluxxor'
jest.dontMock '../../stores/todo-store'
jest.dontMock '../../actions/todo-actions'
describe 'TodoItem', ->
it 'renders a span element with its props', ->
React = require 'react/addons'
TodoItem = require '../todo-item'
Flux = require '../../fluxxors/todo-fluxxor'
TestUtils = React.addons.TestUtils
#render TodoItem instance in virtual DOM
todoElement = TestUtils.renderIntoDocument(
React.createElement TodoItem, {
todo:
text: 'Go to the store'
id: 'j89877787'
complete: false
flux: Flux
}
)
span = TestUtils.findRenderedDOMComponentWithTag(
todoElement, #DOM tree
'span'
)
#Span element should initially render red
expect(span.getDOMNode().textContent).toEqual('Go to the store')
expect(span.getDOMNode().style.color).toEqual('red')
TestUtils.Simulate.click(span)
expect(span.getDOMNode().textContent).toEqual('Go to the store')
expect(span.getDOMNode().style.color).toEqual('green')
For easy reference, here's the TodoItem component, which is a child component of the app.
Fluxxor = require 'fluxxor'
React = require 'react/addons'
FluxMixin = Fluxxor.FluxMixin(React)
module.exports = React.createClass
mixins: [FluxMixin]
propTypes: {
todo: React.PropTypes.object.isRequired
}
handleClick: ->
@getFlux().actions.toggleComplete(@props.todo)
render: ->
{ span } = React.DOM
spanStyle =
if @props.todo.complete
then {color: "green"}
else {color: "red"}
(span {
onClick: @handleClick,
style: spanStyle
}, @props.todo.text)
Perfect, thanks, this helps make things much more concrete.
To start with, you can't really "unit test" that clicking the component turns it green—this involves multiple units (the flux action, the resulting store change, change event emission, proper re-rendering). It is a great candidate for an integration-style test, though. In the meantime, what you can unit test are the following units, which make up that overall functionality:
- When the passed todo is complete, the span is green
- When the passed todo is not complete, the span is red
- When the span is clicked, some specified function is called
The first two items are straightforward:
describe 'TodoItem', ->
it 'renders a green span element with a complete todo', ->
React = require 'react/addons'
TodoItem = require '../todo-item'
TestUtils = React.addons.TestUtils
#render TodoItem instance in virtual DOM
todoElement = TestUtils.renderIntoDocument(
React.createElement TodoItem, {
todo:
text: 'Go to the store'
id: 'j89877787'
complete: true
flux: {}
}
)
span = TestUtils.findRenderedDOMComponentWithTag(
todoElement, #DOM tree
'span'
)
expect(span.getDOMNode().textContent).toEqual('Go to the store')
expect(span.getDOMNode().style.color).toEqual('green')
it 'renders a red span element with an incomplete todo', ->
React = require 'react/addons'
TodoItem = require '../todo-item'
TestUtils = React.addons.TestUtils
#render TodoItem instance in virtual DOM
todoElement = TestUtils.renderIntoDocument(
React.createElement TodoItem, {
todo:
text: 'Go to the store'
id: 'j89877787'
complete: false
flux: {}
}
)
span = TestUtils.findRenderedDOMComponentWithTag(
todoElement, #DOM tree
'span'
)
expect(span.getDOMNode().textContent).toEqual('Go to the store')
expect(span.getDOMNode().style.color).toEqual('red')
The third one is more interesting. At a first glance, the simplest thing to do would be to mock out the action that you want to test. Pseudocode here:
describe 'TodoItem', ->
# ...
it 'calls the appropriate action when clicked', ->
React = require 'react/addons'
TodoItem = require '../todo-item'
todo =
text: 'Go to the store'
id: 'j89877787'
complete: true
flux:
actions:
toggleComplete: createMockFunction("actions.toggleComplete") # depends on test lib
TestUtils = React.addons.TestUtils
#render TodoItem instance in virtual DOM
todoElement = TestUtils.renderIntoDocument(
React.createElement TodoItem, {
todo: todo
flux: flux
}
)
span = TestUtils.findRenderedDOMComponentWithTag(
todoElement, #DOM tree
'span'
)
TestUtils.Simulate.click(span)
expect(flux.actions.toggleComplete).toHaveBeenCalledWith(todo)
However, one of the things I mentioned above was
Ideally, the components that touch flux should be minimal and only pass data/callbacks into more functionally pure components
Said another way, I think that it's best if TodoItem
doesn't actually even know about flux; all it knows about is how to render a todo, and that it should call some passed function when clicked. Here's such an implementation (again, pseudocode):
React = require 'react/addons'
module.exports = React.createClass
propTypes: {
todo: React.PropTypes.object.isRequired
onClick: React.PropTypes.func.isRequired
}
handleClick: ->
@props.onClick(@props.todo)
render: ->
{ span } = React.DOM
spanStyle =
if @props.todo.complete
then {color: "green"}
else {color: "red"}
(span {
onClick: @handleClick,
style: spanStyle
}, @props.todo.text)
So now TodoItem
is completely reusable, and to unit test it we don't have to do anything at all involving flux or Fluxxor; just pass it a todo and a mock function, and test that clicking it calls the function with the given todo.
Now, though, we need a way to get the data out of flux and into the reusable TodoItem
. Its parent is now responsible for this. Here's a parent in the "Container" style, but if you already have a flux-aware parent component, that will work fine too.
# TodoItemContainer or some other parent
Fluxxor = require 'fluxxor'
React = require 'react/addons'
FluxMixin = Fluxxor.FluxMixin(React)
StoreWatchMixin = Fluxxor.StoreWatchMixin
module.exports = React.createClass
mixins: [FluxMixin, StoreWatchMixin("todos")]
getStateFromFlux: ->
{ todos: @getFlux().store("todos").getTodos() }
onTodoClick: (todo) ->
@getFlux().actions.toggleComplete(todo)
render: ->
{ div } = React.DOM
(div {}, @state.todos.map(@renderTodo))
renderTodo: (todo) ->
(TodoItem {
key: todo.id,
todo: todo,
onClick: @onTodoClick
})
So TodoItemContainer
's only purpose in life is to connect a bunch of TodoItem
s with our flux setup. To unit test it, you would mock out various pieces of the flux setup (e.g. the store("todos")
method and the actions.toggleComplete(todo)
method) and verify that it creates the right number of TodoItem
s and that it passes it the correct properties.
As a separate unit test, you would test flux.actions.toggleComplete
to make sure it properly dispatches the right action, and test that dispatching that action to the todos store toggles the passed todo's complete
boolean and emits the correct event from the store. These things are part of the data layer, and can be tested entirely without React.
Finally, you could fully test the integration between the various pieces by creating a real Flux instance, passing it to TodoItemContainer
, and then clicking on the span and making sure it turns green.
There are some good resources on this pattern (often referred to as "Container Components"):
- The video I linked in my original response
- Container Components
- Smart and Dumb Components
You can also see this pattern in some recent Fluxxor discussions, such as #117.
I hope this helped a bit! Let me know if something's not clear.
Thanks for the quick, thorough response! On a related note: how would you test Fluxxor stores? I unit tested actions by making a mock dispatch
function on the actions object, and making sure it was called with the appropriate arguments for each function. You can see that below.
jest.dontMock '../todo-actions'
describe 'TodoActions', ->
TodoActions = null
constants = null
beforeEach ->
constants = require '../../constants/todo-constants'
TodoActions = require '../todo-actions'
TodoActions.dispatch = jest.genMockFunction()
afterEach ->
TodoActions = null
it 'has an addTodo method that calls dispatch with 2 args', ->
TodoActions.addTodo 'go to the supermarket'
firstCall = TodoActions.dispatch.mock.calls[0]
firstArg = firstCall[0]
secondArg = firstCall[1]
expect(firstArg).toBe(constants.ADD_TODO)
expect(secondArg).toEqual({text: 'go to the supermarket'})
it 'has a toggleComplete method that calls dispatch with 2 args', ->
todoItem = {text: 'blah', id: "fijsf", complete: false}
TodoActions.toggleComplete(todoItem)
firstCall = TodoActions.dispatch.mock.calls[0]
firstArg = firstCall[0]
secondArg = firstCall[1]
expect(firstArg).toBe(constants.TOGGLE_TODO)
expect(secondArg).toEqual({todo: todoItem})
it 'has a clearTodos method that calls dispatch with 1 arg', ->
TodoActions.clearTodos()
firstCall = TodoActions.dispatch.mock.calls[0]
firstArg = firstCall[0]
expect(firstArg).toBe(constants.CLEAR_TODOS)
It's a little less clear what the best way to test Fluxxor stores is. For example, I made a TodoStore using Fluxxor.createStore
(code shown below). How do you recommend testing it?
Fluxxor = require 'fluxxor'
uuid = require 'uuid'
constants = require '../constants/todo-constants'
#constant change event string
CHANGE = 'change'
TodoStore = Fluxxor.createStore
initialize: ->
@todos = {}
@bindActions(
constants.ADD_TODO, @onAddTodo,
constants.TOGGLE_TODO, @onToggleComplete,
constants.CLEAR_TODOS, @onClearTodos
)
onAddTodo: (payload)->
identifier = @_uniqueID()
newTodo =
text: payload.text
id: identifier
complete: false
@todos[identifier] = newTodo
#console.log 'ALL TODOS', @todos
@emit CHANGE
onToggleComplete: (payload)->
payload.todo.complete = !payload.todo.complete
@emit CHANGE
onClearTodos: ->
newTodoObject = {}
for id, todo of @todos
if not todo.complete
newTodoObject[id] = todo
@todos = newTodoObject
@emit CHANGE
getState: ->
todoArray = (todo for id, todo of @todos)
#console.log('getState TODOARRAY', todoArray)
return {
todos: todoArray
}
_uniqueID: ->
uuid.v4()
module.exports = TodoStore
Store testing isn't as nice as it could be due to some early API design decisions I made (which I want to correct in a future version). The correct answer is that you should call the store's action callback with the action you want to simulate, the same way the dispatcher does. However, in Fluxxor, that method is not exposed publicly, though you can access it on store#__handleAction__(action)
(where action
has type
and payload
keys).
As an alternative, you can create a new Fluxxor.Flux
instance containing your store and dispatch actions through the dispatcher via flux.dispatcher.dispatch(action)
.
Great, I tried the second approach since __handleAction__
isn't exposed publicly. When I created my flux instance, I did it without providing actions (i.e. flux = new Fluxxor.Flux(stores)
). I figured this would be okay since I'm unit testing the stores, calling flux.dispatcher.dispatch(action)
directly like you recommended. Do you see any problems with that approach for unit testing Fluxxor stores? I didn't want to mix an actions requirement with my unit tests for stores. Thanks again for your help!
Yes, I think that's fine. And in fact, since stores can call waitFor
and other methods on this.flux
during their action handling lifecycle, it's possible that simply calling __handleAction__
wouldn't work in some cases. This is clearly an area that needs some cleanup.
What do you have in mind to clean things up? Maybe I could submit a pull request. :-)
I believe that the root problem is that waitFor()
and store()
are both exposed on the Fluxxor.Flux
instance, which means that the stores are tightly coupled to this.flux
for that sort of functionality. I think a good solution (without relying on global singletons) would be to pass the appropriate objects into the store action callback. The dispatcher could be told programmatically what to pass to the callback.
This is related to a few other decoupling tasks I have in mind that would change the API and warrant a major version bump, but this might be a piece that could be pushed through without removing existing APIs. My only concern is making sure the change fits in to the other API changes cleanly. You can see a very early and scattered list of notes/ideas in the fluxxor-future branch. Please be warned that this is all pretty loose and certainly non-final. :)
I just wrote tests (available here) for the example Todo app in the quick start guide. I feel like it would be helpful for others to add sample tests to the documentation (or at least a link to my quick start guide tests) since it was a little unclear how to test Fluxxor apps. What do you think?
Here's a interesting side effect I noticed today where a store's methods are bound to actions and can't be spied on, however, the store's method is still available (directly). I'm guessing that this is the result of the bindActions here:
var TrackStore = Fluxxor.createStore({
initialize: function() {
this.state = {};
this.bindActions(
constants.TRACK_CURRENT_PAGE, this.trackCurrentPage
);
},
...
trackCurrentPage: function() {
console.log('tracked');
}
});
this is contrived, but imagine you have an action method that dispatches a store constant. The test shows that you can attach a spy to the receiving store's trackCurrentPage, but it will never be called directly.
it.only('expect component.signIn() to work as expected', function() {
var TestContext = require('./../../lib/TestContext').getRouterComponent(MyComponent, {}, true),
component = TestContext.component,
flux = TestContext.flux;
//these spies can be attached with no problems
sinon.spy(flux.actions.AnalyticsActions.track, 'click');
sinon.spy(flux.stores.TrackStore, 'trackCurrentPage');
//now run the component method which triggers the action above
component.signIn();
//the action method is indeed calledOnce, but the store method is not
assert(flux.actions.AnalyticsActions.track.click.calledOnce);
assert(flux.stores.TrackStore.trackCurrentPage.calledOnce); //false
//teardown / restore
flux.actions.AnalyticsActions.track.click.restore();
flux.stores.TrackStore.trackCurrentPage.restore();
});
I guess my follow on question is how do I actually assert that the store method was called? For counter-example, this direct test of the method on the store object is valid.
it('checks a store method', function(){
//spy
sinon.spy(window.flux.stores.TrackStore, 'trackClick');
//exe
window.flux.stores.TrackStore.trackClick();
//assert
assert(window.flux.stores.TrackStore.trackClick.calledOnce); //true
//teardown
window.flux.stores.TrackStore.trackClick.restore();
});
So this may be odd questions but did the jest syntax change? Some of the stuff I read all begin with test()
test('example', () => {
});
I am trying to write a test where if I fire an action the right objects get put into the store. What is the best way to do this today?