Testing React Components that Rely on Third Party Services

Background on the Problem

Recently, while working on a project that implemented React components, I ran into a problem running tests because one of the React components relied on a third party service that was attempting to run during the tests.

The project was a music venue review site for the Boston area called “Doors at Eight”. The project was mainly built using Ruby on Rails and the ERB templating engine. However, a few pieces of the page were React components. The one that is the focus of this post was the component on the homepage. This component use the Pusher API to update the list of recent reviews as soon as a new review was published to the site. Pusher is a service that makes it easy to add websocket features to a site. You can quickly create channels and subscribe to those channels on the browser. On the server, you can push new events to that channel that are immediately reflected in the browser.

Auto updating homepage for Doors At Eight
Auto updating homepage for Doors At Eight

To test the React components, I was using the Karma test runner with Jasmine and Enzyme. Setting these up is beyond the scope of this post, but all of those projects have excellent documentation for getting started.

Original Component

The original component directly imported an ES6 class I wrote to wrap the Pusher service.

// ./components/RecentReviews.js
// Some parts of the code have been removed to focus on relevant pieces
import React, { Component } from 'react';
import PusherService from '../lib/PusherService';

class RecentReviews extends Component {
  constructor(props) {
    super(props);

    // Notice how the PusherService is imported and new'ed up directly
    // inside of the component
    new PusherService(this.receiveNewReview);
  };

  render () {
    return(
      <div className="review-list row">
        {this.populateReviews()}
      </div>
    );
  }
}

export default RecentReviews;

There were no props passed in to this main component when it was rendered to the page.

// ./main.js
ReactDOM.render(
  <RecentReviews />,
  recentReviewsElement
);

Running tests on this component resulted in the Pusher service getting called every time the test ran which both slowed down tests and counted against our available call limits on the API service.

Solution

After a little research and a lot of trial and error, the best solution was to extract the dependency on the Pusher service out of the component and instead inject that dependency when the component is added to the DOM. This meant that during testing, a fake version of the Pusher service could be injected so that the API is never actually called.

Notice how the Pusher class is obtained from the props instead of imported directly in this version.

// ./components/RecentReviews.js
import React, { Component } from 'react';
import Review from './Review.js';

class RecentReviews extends Component {
  constructor(props) {
    super(props);

    this.pusherService = props.pusherService;
    this.pusherService.config(this.receiveNewReview);
  };

  render () {
    return(
      <div className="review-list row">
        {this.populateReviews()}
      </div>
    );
  }
}

export default RecentReviews;

Now, the correct service can be imported and passed through as a prop when the component is mounted. On the site, the actual PusherService class is injected.

import React from 'react';
import ReactDOM from 'react-dom';
import RecentReviews from './components/RecentReviews.js';
import PusherService from './lib/PusherService.js';

ReactDOM.render(
  <RecentReviews pusherService={new PusherService()}/>,
  recentReviewsElement
);

However, in the test, a fake version of the PusherService that implements the same methods, but does not actually call the Pusher service, is injected.

import RecentReviews from 'components/RecentReviews';
import Review from 'components/Review';
import FakePusherService from 'lib/FakePusherService';

describe('RecentReview', () => {
  let wrapper;

  beforeEach(() => {
    jasmine.Ajax.install();
  })

  afterEach(function() {
    jasmine.Ajax.uninstall();
  });

  it('should display a list of reviews returned from an ajax call', () => {
    // The FakePusherService can be injected during a test to avoid
    // calls to the actual Pusher service.
    wrapper = mount(
      <RecentReviews pusherService={new FakePusherService()}/>
    );

    expect(wrapper).toContainReact(<Review venueId={3}
                                           venueName={'Awesome Place'}
                                           body={'This place is great'}
                                           rating={2} />)
    expect(wrapper).toContainReact(<Review venueId={4}
                                           venueName={'Nice Venue'}
                                           body={'This is another great place'}
                                           rating={3} />)
  });
});

Summary

Extracting dependencies out of components and passing those dependencies in as props makes the React component more flexible and reduces hard-coded dependencies on other libraries. As a bonus, this approach allows us to swap out the Pusher service with a different one (e.g., Rails 5 Action Cable) without actually modifying the original component. The strategy of Dependency Injection can be used to make React components that rely on third party services more easily testable.


Brought to you by CEUHelper - Simple, inexpensive, and paperless CEU management for conferences.





Share this story