-
Notifications
You must be signed in to change notification settings - Fork 154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to test Fluxxor apps #119
Comments
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 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 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:
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
Said another way, I think that it's best if 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 Now, though, we need a way to get the data out of flux and into the reusable # 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 As a separate unit test, you would test Finally, you could fully test the integration between the various pieces by creating a real Flux instance, passing it to There are some good resources on this pattern (often referred to as "Container 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 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 = 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 As an alternative, you can create a new |
Great, I tried the second approach since |
Yes, I think that's fine. And in fact, since stores can call |
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 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()
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? |
I'm using React with Fluxxor mixins. What's the recommended way to test my React components that use Fluxxor?
The text was updated successfully, but these errors were encountered: