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

Need Clarification for use with componentDidMount #75

Closed
Jack-Barry opened this issue Sep 17, 2017 · 4 comments
Closed

Need Clarification for use with componentDidMount #75

Jack-Barry opened this issue Sep 17, 2017 · 4 comments

Comments

@Jack-Barry
Copy link

Maybe I am just a bit illiterate when it comes to testing, but I was hoping to get some clarification and/or help on writing tests for calls made by axios from within the componentDidMount function for React components.

Here is a simple componentDidMount function which uses axios:

componentDidMount() {
  axios.get(`http://www.reddit.com/r/${this.props.subreddit}.json`)
    .then((response) => {
      const posts = response.data.data.children.map(obj => obj.data)
      this.setState({ posts })
    })
    .catch((error) => {
      this.setState({ errors: this.state.errors.concat(['GET Request Failed'])})
    })
}

and here is the setup and spec for the functionality:

it('returns data from API as expected', () => {
  let mock = new MockAdapter(axios)

  mock.onGet('http://www.reddit.com/r/test.json')
    .reply(
      200,
      {
        data: {
          children: [
            { data: { id: 1, title: 'A Post' } },
            { data: { id: 2, title: 'Another Post' } }
          ]
        }
      }
    )
  
  let posts = mount(<AjaxDemo subreddit="test" />).update().state().posts
  expect(posts).to.eql([{id: 1, title: 'A Post'},{id: 2, title: 'Another Post'}])
})

The problem I run into is that within the spec, state never changes.

As far as I understand it, when the AjaxDemo component gets mounted, the axios call it makes will receive a response from mock, and update state according to the response from mock. This would mean that state should have been set, because the axios promise resolved and assigned an array of values to state.posts. Is there something I'm missing here? I feel like it's going to be embarrassingly obvious, or maybe there is a better way to split this testing up - maybe it's leaning too much into the integration test arena.

Any help on this would be much appreciated!

An extra note:
I tried running this code with

mount(<AjaxDemo subreddit="test" />).update().state().posts

as well as

mount(<AjaxDemo subreddit="test" />).state().posts

and the call to update() did nothing noticeable.

@ctimmerm
Copy link
Owner

ctimmerm commented Nov 9, 2017

The problem is probably that:

  • you mount the component
  • you fire off a request, which is asynchronous
  • you check the state before the request promise resolves

I'll close this issue since it's not related to axios-mock-adapter.

@ctimmerm ctimmerm closed this as completed Nov 9, 2017
@jrodl3r
Copy link

jrodl3r commented Jan 17, 2018

@ctimmerm would be really nice if a solution was provided instead of a list of problems.

It's nice to check the response and all, but what about the component, and how it was affected after the state changed? What about testing the display of response-triggered elements? Don't we want to test "deeper" than just the response data?


@Jack-Barry After a little tinkering, this is what worked for me:

import React from 'react';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { mount, shallow } from 'enzyme';
import Clients from './Clients';

describe('Clients', () => {
  let clients, mock;
  const clientData =
    [{'_id': '123', 'name': 'CBS', 'address': '100 Broadway Ave, New York, NY 15395'}, 
     {'_id': '456', 'name': 'NBC', 'address': '4688 19th Street, New York, NY 12536'}];

  beforeEach(() => {
    mock = new MockAdapter(axios);
    mock.onGet('api/clients').reply(200, clientData);
    clients = mount(<Clients />);
  });

  it('gets client data and updates the state', () => {
    clients.update();
    expect(clients.state()).toHaveProperty('clients', clientData);
    expect(clients.find('.card-body').length).toEqual(1);
    expect(clients.find('.empty-text').length).toEqual(0);
    expect(clients.find('table').length).toEqual(2);
    expect(clients.find('tr').length).toEqual(3);
  });

});

🍻

@ctimmerm
Copy link
Owner

It was not a list of problems, but a sequence of events that explains the problem.

It is not a problem that is related to axios-mock-adapter and the way you handle it depends on the testing framework and other libraries you're using, and how you structure your code.
If you're using mocha, the simplest solution might be to wrap your assertion in setImmediate or setTimeout.

it('does something', function(done) {
  // ... your test code ...

  setImmediate(function() {
    expect(/* ... */);
    done();
  });
});

@Jack-Barry
Copy link
Author

@jrodl3r Thanks for the tips! I actually rethought my approach after the correct albeit blunt response from @ctimmerm and just forgot to come back and provide the solution I came up with. I understand that this isn't an issue with the functionality of axios-mock-adapter, but figured since it's likely a very common use case that it would be something that should be discussed here for others to be able to skip the beating-head-against-wall phase of figuring it out.

Separation of Concerns

I ended up writing the methods that make use of axios completely decoupled from the React code, then stubbed out those methods when testing how the React component will respond to the data.

Build a Method to Make the AJAX Call

Let's say I write a method called myApiGet that uses axios.get. I'll test that method on its own using axios-mock-adapter to make sure that it does what I expect it to with whatever data is returned as a result of the axios call. For example I might test to ensure that myApiGet returns an object that contains the HTTP response code along with the expected data.

Use the Predictable AJAX Call Method in componentDidMount

For the React components, I stub out what's expected to be returned by myApiGet using sinon, and use this stubbed response to test the behavior of the component.

Bennies

This seems to be the best approach to separate concerns that I could come up with and offers the following benefits:

  • The method that uses axios can be reused throughout the codebase and can be expected to return data in a predictable format
  • The code in the component is tested against the expected behavior of the method that uses axios, and thus only needs to be tested for what it does with the data returned

Conclusion

What I was running into was that if I wrote the axios calls into componentDidMount, I needed to test that the call was not only returning the data as expected but also that the component is rendering the data as anticipated all within the same spec, and it just seems messy. The way I understand it:

  1. A React component should do one thing well: render data in the view layer.
  2. The methods used for AJAX calls should do one thing well: return data in a predictable format.

Once again, this isn't necessarily an issue with the functionality of axios-mock-adapter, but hopefully anyone looking into this will help others dealing with similar problems.

Here is a thread where we discussed how to deal with rendering data that should be available from componentDidMount.

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

3 participants