Skip to content
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

Open
techapman opened this issue May 18, 2015 · 12 comments
Open

How to test Fluxxor apps #119

techapman opened this issue May 18, 2015 · 12 comments

Comments

@techapman
Copy link

I'm using React with Fluxxor mixins. What's the recommended way to test my React components that use Fluxxor?

@BinaryMuse
Copy link
Owner

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.

@techapman
Copy link
Author

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)

@BinaryMuse
Copy link
Owner

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 TodoItems 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 TodoItems 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"):

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.

@techapman
Copy link
Author

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

@BinaryMuse
Copy link
Owner

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).

@techapman
Copy link
Author

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!

@BinaryMuse
Copy link
Owner

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.

@techapman
Copy link
Author

What do you have in mind to clean things up? Maybe I could submit a pull request. :-)

@BinaryMuse
Copy link
Owner

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. :)

@techapman
Copy link
Author

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?

@adjavaherian
Copy link

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();
     });

@tachang
Copy link

tachang commented Feb 19, 2018

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants