Chapter 9. Testing Redux applications

This chapter covers

  • Introducing testing tools
  • Strategies for testing Redux building blocks
  • Testing advanced Redux features

Instead of testing each Parsnip feature as it was built, our strategy has been to save it all up for one comprehensive chapter. Ideally, this chapter serves as a convenient reference manual for all your Redux testing needs going forward. In the coming sections, we cover common testing tools and strategies for testing action creators, reducers, and components. We also work through examples for testing advanced features: middleware, selectors, and sagas. Feel free to skip around as needed.

Because this chapter is intended to be easily referenced outside the context of the Parsnip application, examples will be inspired by code written for Parsnip but may be pared down to make a clearer point. In the process, you’ll gain the knowledge and tools necessary for testing Parsnip code, so the exercise at the end of the chapter asks you to do that. You’ll check your understanding by testing a specific feature. As you read through the chapter, consider how the lessons could be extended to test related functionality in Parsnip or your own application.

Good news! Most of this chapter is straightforward and easy to understand and apply. As you’ll recall, Redux has the great advantage of decoupling application logic from view rendering. When separated into its constituent parts, each element of the Redux workflow is relatively simple to test in isolation. Where possible, Redux encourages the writing of pure functions, and it doesn’t get any easier than testing pure functions.

There will be times where testing becomes unavoidably complex. Think back to each of the points in the workflow where side effects are managed, and you’ll have a good idea of where it can get a little hairy. Fortunately, you can keep the testing complexity in one place, just like the implementation.

A note on test-driven development

Before writing Redux full-time, both of us held the steadfast belief that test-driven development (TDD) was the one true path. When it comes to writing Ruby, Go, or a different JavaScript framework, we still advocate using TDD in certain situations. Particularly when working on the back end, having tests sometimes provides the quickest feedback loop for development. The value proposition for TDD in React and Redux applications is dramatically less clear.

Throughout the book, we’ve practiced and preached the workflow of building the UI before connecting it to Redux. All too often though, the component composition for a new feature will evolve as you’re feeling out the feature. Groups of UI elements will get extracted out into new components along the way—that sort of thing. If you took a stab at the component tests before starting their implementation, more often than not, you end up rewriting those tests.

As we hope you can now attest, the development experience in Redux is outstanding. Hot module replacement provides you with immediate visual feedback as you develop, while preserving the application state, and the Redux DevTools spell out every state change coming down the pipeline. Client-side development cycles have never been faster, and for that reason, we prefer not to write component tests in advance of sketching out the components.

More of an argument can be made for writing action creator, reducer, and other tests in advance, though. If those elements are test-driven, you can expect the typical benefits for those domains: better consideration of sad paths, better overall code coverage, and a baseline for code quality. A deeper discussion of the merits of test-driven development is outside the scope of this book, but if it’s a value in your organization, our recommendation is to try it with components last, lest you have a bad time and quit before making it to action creators and reducers.

9.1. Introduction to testing tools

You have many options when it comes to JavaScript testing. A common complaint from developers moving to the JavaScript ecosystem from another programming language is a lack of strong conventions. Choice means flexibility, but it also means mental overhead. The React community has attempted to address this concern by including a robust testing utility, Jest, in apps generated by Create React App. When you create a new application with the CLI tool, Jest is installed and configured as a part of that process. You’re free to use any test framework you prefer, but in this chapter, you’ll run with the default and use Jest as the test runner and assertion library.

As a beginner-friendly convenience, Create React App abstracts many of the application’s configuration settings away using the react-scripts package. To view the configuration settings requires ejecting from Create React App. Ejecting removes the abstractions and gives you access to the raw configuration files, enabling you to make your own tweaks.

The command for ejecting is npm run eject. Once you eject, there’s no turning back, so the CLI will give you a final warning before performing the ejection. If you want to look around an ejected application, but don’t want to eject your Parsnip application, spin up a new application using create-react-app new-app and perform the ejection there.

Within your ejected application, look at the package.json file for the configuration details within the jest key. Take a deep breath first, because there’s a lot going on. The good news is that you didn’t have to write this from scratch, and the settings are straightforward enough to understand. The following listing breaks down the configuration settings.

Listing 9.1. The package.json file after ejecting from Create React App
...
"jest": {
    "collectCoverageFrom": [                                       1
      "src/**/*.{js,jsx}"
    ],
    "setupFiles": [                                                2
      "<rootDir>/config/polyfills.js"
    ],
    "testMatch": [                                                 3
      "<rootDir>/src/**/__tests__/**/*.js?(x)",
      "<rootDir>/src/**/?(*.)(spec|test).js?(x)"
    ],
    "testEnvironment": "node",
    "testURL": "http://localhost",
    "transform": {                                                 4
      "^.+\.(js|jsx)$": "<rootDir>/node_modules/babel-jest",
      "^.+\.css$": "<rootDir>/config/jest/cssTransform.js",
      "^(?!.*\.(js|jsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns": [                                   5
      "[/\\]node_modules[/\\].+\.(js|jsx)$"
    ],
    "moduleNameMapper": {
      "^react-native$": "react-native-youb"
    },
    "moduleFileExtensions": [                                      6
      "youb.js",
      "js",
      "json",
      "youb.jsx",
      "jsx",
      "node"
    ]
  },
...

  • 1 Shows test coverage for the files that match this pattern
  • 2 Executes code before running the test suite
  • 3 Tests files that match these patterns
  • 4 Applies transformers, like Babel, to certain file extensions
  • 5 Leaves the node_modules directory alone
  • 6 Jest should handle imports with these filename extensions, even if the extension isn’t explicitly stated.

It’s not crucial for you to understand what’s happening under the hood, and that’s exactly why Create React App hides these details by default. What’s important is that when you run the test command (npm test), Jest will run any tests in files that are within a __tests__ directory or end in .test.js, .test.jsx, .spec.js, or .spec.jsx. If you prefer to tweak those settings, that’s what ejecting is intended for.

If you have experience with the Jasmine testing framework, Jest will look familiar. Jest is a set of features built on top of Jasmine. However, as of May 2017, Jest maintains its own fork of Jasmine. The intention there is to maintain greater control over their own test runner and to add, change, or remove functionality as it meets the needs of Jest.

9.1.1. What does Jasmine provide?

Jasmine (https://jasmine.github.io/) describes itself as a behavior-driven development (BDD) framework for testing JavaScript. Paraphrasing its intention, BDD emphasizes writing human-readable tests focused on the value provided by the feature you’re testing. For the purposes of this chapter, that’s roughly all you’ll need to know about BDD. Among other things, Jasmine provides the syntax to write tests and make assertions. Listing 9.2 provides an example of that syntax.

The describe function exists to group a series of related tests that can be nested to define more nuanced relationships between tests. As an imperfect rule of thumb, each file you test should result in one or more describe blocks.

Within a describe block is one or more test cases declared using the it function. If the describe function defines the noun being tested, the it function defines the verb.

Listing 9.2. Example Jasmine test
describe('a generic example', () => {                          1
  it('should demonstrate basic syntax', () => {                2
    expect(5 + 1).toEqual(6);                                  3
  });

  it('should also demonstrate inverse assertions', () => {     4
    expect(true).not.toBe(false);
  });
});

  • 1 The “describe” function provides context for a series of related tests.
  • 2 The “it” function denotes a unit test.
  • 3 Syntax for making assertions
  • 4 Multiple unit tests reside within a “describe” block.

The tests are easy to read, aren’t they? That’s the intention of BDD, making test cases readable by even non-technical stakeholders. Jasmine comes packaged with a command-line test runner that uses the describe and it blocks to generate especially readable output.

If the first test in listing 9.2 were updated to expect(5 + 3).toEqual(6), and you ran the test suite with Jasmine, the abbreviated terminal output would look something like this:

Failures:
1) a generic example should demonstrate basic syntax
  Message:
    Expected 8 to equal 6.

The friendly test output makes it easy to run a test suite and, in plain English, understand much of what functionality the application has or hasn’t implemented.

9.1.2. What does Jest provide?

If Jasmine is such a nice tool, why bother with Jest? For one, it works out of the box with React, saving you from having to install and configure something similar to jsdom. Jest keeps the syntax and test runner as a foundation but extends Jasmine in many significant ways. The test runner, for example, has been beefed up to provide more detailed output, a watch mode to automatically replay only tests affected by a code change, and a code coverage tool.

Figure 9.1 shows you an example of the watch mode test output. The tests will run every time you save a file, but notice that you can also specify options for running specific tests by file or test name from the runner. This allows you to run only the subset of tests you care about for the feature or bug you’re working on.

Figure 9.1. Example watch mode output from Jest

Beyond the runner, Jest implements performance improvements and has a powerful feature called snapshot testing. This is an optional tool that can come in handy when testing components. We’ll look at snapshots later in the chapter.

If you try running the test command (npm test) in Parsnip, you’ll notice that a single test will run and fail. This is to be expected, because you’ve paid testing no mind up to this point. Delete the test file found at src/App.test.js.

9.1.3. Alternatives to Jest

If you prefer another testing framework, use it! Once you’ve installed your tool of choice, using it instead of Jest is as simple as changing the test script in the package.json file. Several of the most popular alternatives to Jest include Jasmine proper, Mocha, AVA, and Tape.

The Mocha framework (https://mochajs.org) is among the most popular in the JavaScript community, but doesn’t come with “batteries included,” as Jasmine likes to tout. As a result, you’ll commonly see Mocha paired with an additional assertion library, such as Chai (http://chaijs.com/) or Expect (https://github.com/mjackson/expect), to cover the ground that Mocha doesn’t.

AVA (https://github.com/avajs/ava) is newer to the scene. It’s received attention for having a slim interface, a slick watch mode, and first-class performance, thanks to tests running concurrently by default.

Finally, Tape (https://github.com/substack/tape) is another newer entrant. It’s been embraced for its simplicity, lack of frills, and speed of integration. For these reasons, other open source packages often use Tape for quick examples in their project documentation.

Naturally, this isn’t an exhaustive list of testing framework options. By the time this book is published, it’s likely you’ll have half a dozen more to choose from. Go with whichever makes the best sense for your project and your team. It’s outside of the scope of this chapter to provide installation and configuration instructions for these tools, so reference your respective framework’s documentation for those details.

9.1.4. Component testing with Enzyme

One testing tool that seemingly all React developers can agree on is Enzyme. Enzyme (http://airbnb.io/enzyme/) is a testing tool that makes it dramatically easier to test the output of React components. You can make assertions about the existence of DOM elements, prop values, state values, or callbacks firing on a click event, for example. The syntax is friendly, especially when compared with the utility it wraps, ReactTestUtils (https://facebook.github.io/react/docs/test-utils.html). If you’re interested in testing that three buttons rendered within a component, the syntax looks roughly like this:

expect(wrapper.find('button').length).toEqual(3);

Enzyme’s syntax draws a comparison with jQuery. In this case, the find method functions in much the same way it does in jQuery, returning the array of items that match the query. At that point, you’re free to use Jest to make the assertion that the number of buttons is equal to three. Later in the chapter, we’ll cover more functionality offered by Enzyme.

Note

Enzyme underwent a large rewrite between versions 2 and 3, which includes a couple of breaking changes. Version 3 introduces Adapters, meant to make the testing library more extensible for use with React-like libraries, like Preact and Inferno. The examples in this book will use version 3.

9.2. How does testing Redux differ from React?

React, of course, includes only the views—the components. If you’ve tested components in your prior experience with React, you’ve likely used Enzyme along with one of the frameworks and assertion libraries listed in the previous section. Testing components connected to the Redux store works largely the same way. However, you’ll have to account for the augmented functionality of having access to the Redux store in one of two ways, which we’ll detail later in the chapter. We’ll save component testing for last, to underscore the suggestion not to test-drive development with component tests.

Beyond components, the rest of the elements are newly introduced by Redux and will merit separate test files for each: action creators, reducers, selectors, and sagas. Remember that reducers and selectors are pure functions, so testing them will be straightforward. Pass them data, then make assertions about the results.

Action creator tests must account for both synchronous and asynchronous actions. The former are pure functions as well, so they’ll also be easy to knock out, but asynchronous functions will get more involved when you account for side effects such as AJAX requests.

You might be tempted to guess that generators are complicated to test as well, but recall that sagas output only objects called effects, which describe the side effect to be performed by the middleware. Consequently, they’re simpler to test than asynchronous action creators.

In the coming sections, you’ll walk through test examples for each element in the workflow, starting with the action creators. You’ll progress in the sequential order that you’d write the functionality: action creators, sagas, middleware, reducers, and selectors, before circling back to components.

9.3. Testing action creators

This section is broken up into two parts: synchronous and asynchronous action creator tests. As we’ve mentioned, the latter will account for additional complexity, given the need to manage side effects. You’ll start nice and easy with synchronous action creators, though.

9.3.1. Testing synchronous action creators

Synchronous action creators optionally accept arguments and return a plain object, called an action. These are pure functions that produce deterministic results. The following listing introduces a familiar action creator, createTaskSucceeded. Be sure to export the function to make it available to a test file.

Listing 9.3. Example synchronous action
export function createTaskSucceeded(task) {    1
  return {                                     2
    type: 'CREATE_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

  • 1 Exports the action creator for testing
  • 2 The action creator returns an action object.

When deciding where to write your tests, recall that you have a couple of options provided by the Jest configuration. Jest will run the tests in any file that lives in a __tests__ directory, or is suffixed with .test.js(x) or .spec.js(x). This choice is a matter of style preference, with a couple of practical concerns.

Using a dedicated testing directory makes it easier to view and navigate between all your tests. Another option, co-locating tests alongside the files they’re testing, makes it easy to view the tests associated with the feature you’re working on at a given time. Co-locating tests also has the advantage of making it obvious when a file doesn’t have an associated test file. There’s no wrong answer, so try the option that appeals most to you and reorganize if you become dissatisfied with your choice later.

Listing 9.4 offers an example test for the createTaskSucceeded action creator. After importing the function, you’ll define the test context, the noun, using a describe block. The it function denotes a unit test—the verb. The goal of the test is to execute the action creator and assert that its result meets your expectation. The example task and expectedAction variables are extracted for readability but aren’t a requirement. You can choose to organize your test however you prefer, and it doesn’t even necessarily need to have an expect function.

Listing 9.4. Synchronous action creator test
import { createTaskSucceeded } from './actions/';                        1

describe('action creators', () => {                                      2
  it('should handle successful task creation', () => {                   3
    const task = { title: 'Get schwifty', description: 'Show me what you
  got' }
    const expectedAction = { type: 'CREATE_TASK_SUCCEEDED', payload: { task
  } };
    expect(createTaskSucceeded(task)).toEqual(expectedAction);           4
  });
});

  • 1 Imports the action creator to test
  • 2 Provides the domain noun to the describe function
  • 3 Provides the verb to the it function
  • 4 Asserts that the action creator’s output is correct

If Jest can execute an entire unit test without error, it will consider that a passing test; however, we recommend using the test assertions for greater specificity and confidence that your code is fully tested.

9.3.2. Testing asynchronous action creators

Testing asynchronous action creators requires more effort than their synchronous counterparts, but we’ve found async action tests provide good bang for the buck. Async action creators are these great packages of reusable functionality, and, as a result, any bugs can be felt in multiple places across your application. Generally, the more often a software component is reused, the stronger the case for a unit test. When others who may not have as much context as you want to modify an async action creator, tests ensure they can make changes with confidence.

What do you want to test with async actions? Let’s use createTask as an example, one of Parsnip’s core action creators. Using createTask is a standard example of async action that makes a network request. Assuming for the sake of this test that you’d get a successful server response, here are createTask’s responsibilities:

  • Dispatch an action indicating the request has started.
  • Make the AJAX request with the correct arguments.
  • When the request succeeds, dispatch an action with data from the server response.

These sound an awful lot like things you can translate directly into test assertions, and you’ll do that. Before you get to the test, let’s look at the implementation of createTask you’ll be using, as shown in the following listing.

Listing 9.5. The createTask async action creator
export function createTaskRequested() {
  return {
    type: 'CREATE_TASK_REQUESTED'
  }
}

function createTaskSucceeded(task) {
  return {
    type: 'CREATE_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

export function createTask({ title, description, status = 'Unstarted' }) {
  return dispatch => {
    dispatch(createTaskRequested());                                      1
    return api.createTask({ title, description, status }).then(resp => {  2
        dispatch(createTaskSucceeded(resp.data));                       3
    });
  };
}

  • 1 Dispatches an action when the request starts
  • 2 Makes the request and returns the promise returned by api.createTask
  • 3 Dispatches an action when the request succeeds

One thing to note here is that you’re returning the promise returned by api.createTask from within the action creator. This wasn’t strictly required by your implementation, but as you’ll see in a moment, it allows you to make assertions in test. This isn’t a code change made based solely on testing requirements; it’s often a good practice to return promises from your async action creators because it gives callers the flexibility to respond to the result of the promise.

You know roughly what assertions you need to make, but what kind of setup do you need? You’ll need one extra package: redux-mock-store. This gives you a convenient interface, store.getActions() that returns a list of the actions that have been dispatched to the mock store. You’ll use this to assert that the request start/success actions are dispatched properly by createTask. The only other thing you’ll need is Jest, which you’ll use to manually mock the API response. Let’s start by configuring the mock store, which you’ll eventually use to dispatch createTask. Also import and apply the redux-thunk middleware, which createTask depends on, as shown in the following listing.

Listing 9.6. Configuring a mock Redux store
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk';
   import { createTask } from './';

const middlewares = [thunk];                             1
const mockStore = configureMockStore(middlewares);       1

  • 1 Creates the mock store with the redux-thunk middleware

Next you’ll mock api.createTask, the function responsible for making the AJAX request. You’re using Jest to mock the function out entirely, but you’ll commonly see HTTP-mocking libraries such as nock used for similar purposes. HTTP mocking has the benefit of being slightly more of an integration test, because an additional component (the API call) is involved directly in the test. The downside is that it can occasionally lead to more overhead in test creation and maintenance. Flat out mocking the function that makes the API call means the action creator test is more focused, but it also means that it won’t catch any bugs related to the AJAX call.

For now, you’ll mock api.createTask directly using Jest’s mocking utilities, as shown in the following listing. This ensures api.createTask will return a promise that you control directly in the test, and you won’t have to worry about anything HTTP related.

Listing 9.7. Mocking api.createTask
...
jest.unmock('../api');                                                1
import * as api from '../api';                                        2
api.createTask = jest.fn(                                             3
  () => new Promise((resolve, reject) => resolve({ data: 'foo' })),   3
);                                                                    3
...

  • 1 Opts out of Jest’s auto-mocking
  • 2 Imports the api module
  • 3 Explicitly mocks the api.createTask function to return a promise

You’ve got most of the setup out of the way; now it’s time to get into the meat of the test. Use the mock store to dispatch the createTask action creator and make assertions about the actions dispatched in the process, as shown in the following listing.

Listing 9.8. Testing createTask
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { createTask } from './';

jest.unmock('../api');
import * as api from '../api';
api.createTask = jest.fn(
  () => new Promise((resolve, reject) => resolve({ data: 'foo' })),
);

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe('createTask', () => {
  it('works', () => {
    const expectedActions = [                                       1
      { type: 'CREATE_TASK_STARTED' },
      { type: 'CREATE_TASK_SUCCEEDED', payload: { task: 'foo' } },
    ];

    const store = mockStore({                                       2
      tasks: {
        tasks: [],
      },
    });

    return store.dispatch(createTask({})).then(() => {              3
      expect(store.getActions()).toEqual(expectedActions);          4
      expect(api.createTask).toHaveBeenCalled();                    5
    });
  });
});

  • 1 Creates an array of actions you expect to be dispatched by createTask
  • 2 Creates the mock store
  • 3 Uses the mock store to dispatch createTask
  • 4 Uses the store.getActions method, which will return a list of actions that have been dispatched
  • 5 Asserts that createTask makes the AJAX request

It required a good deal of setup, but all in all this was a reasonable unit test for an async action creator. They tend to have non-trivial functionality and can be difficult to test due to having so many dependencies (for example the Redux store, AJAX). Between redux-mock-store and Jest, the resulting test code isn’t over-complicated.

Remember, if the setup starts to become tedious, you can always abstract common work into test utilities.

9.4. Testing sagas

As a quick review, Redux sagas are an alternative pattern to thunks for handling side effects. They’re best used for handling more complex side effects, such as long-running processes. In chapter 6, you wrote a saga to manage a timer that kept track of how long a task was in progress. In this section, you’ll lean on that saga as an example to test.

Listing 9.9 introduces enough code for the sake of this discussion. You’ll see the imports from the root package and the effects helper methods, in addition to the handleProgressTimer generator function. As a refresher, the generator function receives an action, and executes code if the action type is TIMER_STARTED. When that’s true, an infinite loop is initiated, and each loop through waits one second before dispatching a TIMER_INCREMENT action, as shown in the following listing.

Listing 9.9. The handleProgressTimer saga
import { delay } from 'redux-saga';                           1
import { call, put } from 'redux-saga/effects';               1
...
export function* handleProgressTimer({ type, payload }) {     2
  if (type === 'TIMER_STARTED') {
    while (true) {                                            3
      yield call(delay, 1000);
      yield put({
        type: 'TIMER_INCREMENT',
        payload: { taskId: payload.taskId },
      });
    }
  }
}

  • 1 Imports helper methods
  • 2 Exports the generator function to test it
  • 3 Until the type changes, wait one second, then dispatch an increment action.

Fortunately, testing generators is simpler than testing most thunks. Remember, the saga middleware is what executes AJAX requests or other side effects. The sagas you write return an effect: an object that describes what the middleware should do. When you test sagas, then, all you need to assert is that the generator function returns the effect object you expect.

After importing the necessary functions, listing 9.10 will test the generator function with a couple of different actions, TIMER_STARTED and TIMER_STOPPED. Keep in mind that each return value of a generator is an object with value and done keys. For the TIMER_STARTED test, you’ll want to assert that the value key meets our expectations each time the next function is called on the generator. However, instead of manually typing out the effect object, you can invoke a saga method that produces the expected output.

Context may be helpful to understand the reasoning behind this decision. Effects are generated by the redux-saga helper methods and aren’t intended to be pretty. Here’s an example of the effect produced by a call of the delay method (for example, call(delay, 1000)):

{
  '@@redux-saga/IO': true,
  CALL: { context: null, fn: [(Function: delay)], args: [1000] },
}

Instead of writing out this object manually, you can more easily assert that the next value produced by the generator function is equal to the result of executing call(delay, 1000), as shown in the following listing.

Listing 9.10. Testing sagas
import { delay } from 'redux-saga';                                        1
import { call, put } from 'redux-saga/effects';                            1
import { handleProgressTimer } from '../sagas';                            1

describe('sagas', () => {
  it('handles the handleProgressTimer happy path', () => {
    const iterator = handleProgressTimer({                                 2
      type: 'TIMER_STARTED',
      payload: { taskId: 12 },
    });

    const expectedAction = {
      type: 'TIMER_INCREMENT',
      payload: { taskId: 12 },
    };

    expect(iterator.next().value).toEqual(call(delay, 1000));
    expect(iterator.next().value).toEqual(put(expectedAction));            3
    expect(iterator.next().value).toEqual(call(delay, 1000));
    expect(iterator.next().value).toEqual(put(expectedAction));
    expect(iterator.next().done).toBe(false);                              4
  });

  it('handles the handleProgressTimer sad path', () => {                   5
    const iterator = handleProgressTimer({                                 6
      type: 'TIMER_STOPPED',
    });

    expect(iterator.next().done).toBe(true);                               7
  });
});

  • 1 Imports the library methods and saga
  • 2 Initializes the generator function with a TIMER_STARTED action
  • 3 Infinitely, the saga waits for one second, then dispatches the action.
  • 4 At any point, this saga will indicate it isn’t done.
  • 5 Tests the case that the generator doesn’t receive a TIMER_STARTED action
  • 6 Initializes the saga with a TIMER_STOPPED action
  • 7 Confirms that the saga is done immediately

If there are forks in your logic, be sure to test each of them. In this case, you need to test the saga in the event that an action other than TIMER_STARTED comes through. The test proves simple, because the body of the generator function is skipped and the done key immediately returns a true value.

Learning to write sagas may not be easy, but happily, testing them is more straightforward. The big idea is to step through the results of the generator function, one next at a time. Assuming there is a conclusion to the saga, eventually you can assert that the value of done is true.

A final note on the subject: sagas respond to actions dispatched from somewhere, so don’t forget to test that dispatch. Typically, this is a synchronous action creator that requires a simple test of a pure function (section 9.4.1). On to middleware!

9.5. Testing middleware

Middleware intercept actions before they reach the reducers. They’re written in a peculiar function signature with three nested functions. The goal of middleware tests is to assess that specific actions are being handled appropriately. For this example, you’ll reference the analytics middleware written in chapter 5, with slight changes.

Listing 9.11 introduces the middleware. The nested function will check for actions with an analytics key. If the action in question doesn’t have any, it gets passed on with the next function. Appropriate actions will move on to trigger an analytics AJAX request.

Listing 9.11. exampleMiddleware – Analytics middleware
import fakeAnalyticsApi from './exampleService';

const analytics = store => next => action => {
  if (!action || !action.meta || !action.meta.analytics) {    1
    return next(action);
  }

  const { event, data } = action.meta.analytics;

  fakeAnalyticsApi(event, data)                               2
    .then(resp => {
      console.log('Recorded: ', event, data);
    })
    .catch(err => {
      console.error(
        'An error occurred while sending analytics: ',
        err.toSting()
      );
    });

  return next(action);                                       3
};

export default analytics;

  • 1 Passes the action on if no analytics key exists
  • 2 Performs an AJAX request
  • 3 Always passes the action to the next middleware or reducers

The API service is mocked out for the purposes of testing. See the following listing for that example code. Every call results in a successful promise resolution.

Listing 9.12. exampleService.js – Example fake analytics API service
export default function fakeAnalyticsApi(eventName, data) {
  return new Promise((resolve, reject) => {                   1
    resolve('Success!');
  });
}

  • 1 Mocked API call for the sake of the example

The mechanics of testing this code are a little cumbersome, but made easier with a helper function provided in the official Redux documentation, found at https://github.com/reactjs/redux/blob/master/docs/recipes/WritingTests.md#middleware. This method is called create and can be seen in listing 9.13. The purpose of the create function is to mock out all the important functions, while providing a convenient wrapper for executing the middleware.

These tests also require more advanced Jest mocking. The API service gets mocked, so that you can assert whether it was called. In the listing, you can see the mocking and importing of the module. The mockImplementation function is used to specify what happens when the mocked function is executed. The mocked value you use is identical to your actual implementation. Your tested version may look like this, but your actual implementation of an API service won’t.

Each test will use the create method to get running, pass in an object to the middleware, and assert whether it interacts with the API service. Because the service is mocked, it keeps track of whether it was invoked.

Finally, all actions should end up as an argument, executed by the next function. This is an important assertion to write for each of the middleware tests.

Listing 9.13. Analytics middleware test
import analytics from '../exampleMiddleware';

jest.mock('../exampleService');                                            1
import fakeAnalyticsApi from '../exampleService';
fakeAnalyticsApi.mockImplementation(() => new Promise((resolve, reject) =>
  resolve('Success')));                                                  2

const create = () => {                                                     3
  const store = {
    getState: jest.fn(() => ({})),
    dispatch: jest.fn(),
  };
  const next = jest.fn();
  const invoke = (action) => analytics(store)(next)(action);
  return { store, next, invoke };
};

describe('analytics middleware', () => {
  it('should pass on irrelevant keys', () => {
    const { next, invoke } = create();                                     4

    const action = { type: 'IRRELEVANT' };

    invoke(action);                                                        5

    expect(next).toHaveBeenCalledWith(action);                             6
    expect(fakeAnalyticsApi).not.toHaveBeenCalled();
  })

  it('should make an analytics API call', () => {
    const { next, invoke } = create();

    const action = {
      type: RELEVANT,
      meta: {
        analytics: {                                                       7
          event: 'foo',
          data: { extra: 'stuff' }
        },
      },
    };

    invoke(action);

    expect(next).toHaveBeenCalledWith(action);
    expect(fakeAnalyticsApi).toHaveBeenCalled();                           8
  })
})

  • 1 Mocks the API service
  • 2 Determines the mock’s response
  • 3 Shows the create helper method from official documentation
  • 4 Uses the create helper to reduce redundancy
  • 5 Sends the action through the middleware
  • 6 Asserts that the action was passed to the next function
  • 7 Provides an action that meets the middleware criteria
  • 8 Asserts that the service was executed

That was pretty dense. Revisit the create method to make sure you grok what that’s doing for you. It’s not too magical and does a good job of DRYing up the test suite. Give the quick documentation a read for more details at https://github.com/reactjs/redux/blob/master/docs/recipes/WritingTests.md#middleware.

Mocking functions is another topic that may not come easy the first couple times around the block. Spending time with the related documentation is a good investment. You’ll find more than you’ll ever need to know at https://facebook.github.io/jest/docs/en/mock-functions.html.

9.6. Testing reducers

Testing reducers is pleasantly straightforward. At a high level, you’ll test for each case in a switch statement. The following listing contains an abbreviated tasks reducer for which you’ll write tests. For the sake of example, the reducer won’t use normalized data. In the listing, you’ll find an initial state object and a tasks reducer with two case clauses and a default clause.

Listing 9.14. Tasks reducer
const initialState = {                                           1
  tasks: [],
  isLoading: false,
  error: null,
  searchTerm: '',
};

export default function tasks(state = initialState, action) {    2
  switch (action.type) {                                         3
    case 'FETCH_TASKS_STARTED': {
      return {
        ...state,
        isLoading: true,
      };
    }
    case 'FETCH_TASKS_SUCCEEDED': {                              4
      return {
        ...state,
        tasks: action.payload.tasks,
        isLoading: false,
      };
    }
    ...
    default: {                                                   5
      return state;
    }
  }
}

  • 1 Provides initial state for the reducer
  • 2 Exports the reducer for usage and testing
  • 3 A switch statement handles each incoming action type.
  • 4 Shows one case clause per action type
  • 5 A default clause is triggered when no action type matches.

Because a reducer is a pure function, testing it requires no special helper methods or any other funny business. Writing reducer tests is a great way to reinforce your understanding of what a reducer does, though. Recall that a reducer takes an existing state and a new action and returns a new state. The signature for that test looks like this:

expect(reducer(state, action)).toEqual(newState);

For each action type, you’ll assert that a new action results in the state you expect. The following listing contains tests for each action type. As demonstrated in the listing, it’s also a good idea to test that the default state appears as you expect it to. That test calls the reducer function with no current state value and an empty action object.

Listing 9.15. Tasks reducer test
import tasks from '../reducers/';

describe('the tasks reducer', () => {                                1
  const initialState = {                                             2
    tasks: [],
    isLoading: false,
    error: null,
    searchTerm: '',
  };

  it('should return the initialState', () => {                       3
    expect(tasks(undefined, {})).toEqual(initialState);
  });

  it('should handle the FETCH_TASKS_STARTED action', () => {
    const action = { type: 'FETCH_TASKS_STARTED' };                  4
    const expectedState = { ...initialState, isLoading: true };      5

    expect(tasks(initialState, action)).toEqual(expectedState);
  });

  it('should handle the FETCH_TASKS_SUCCEEDED action', () => {
    const taskList = [{ title: 'Test the reducer', description: 'Very meta'
 }];
    const action = {
      type: 'FETCH_TASKS_SUCCEEDED',
      payload: { tasks: taskList },
    };
    const expectedState = { ...initialState, tasks: taskList };

    expect(tasks(initialState, action)).toEqual(expectedState);
  });
});

  • 1 The reducer tests are within one describe block.
  • 2 Import or declare initial state for all tests.
  • 3 Tests for the initial state
  • 4 Defines the action passed into the reducer
  • 5 Defines the expected state produced by the reducer

In many of the tests, you’ll see variables for the action and expected state declared prior to the assertion. This pattern is for the convenience of having more readable assertions.

In these tests, you’ll notice that you used initialState as the first argument in the reducer you’re testing. You can wander away from this pattern, and will likely need to, to get full test coverage. For example, the FETCH_TASKS_SUCCEEDED test captures the change to tasks in the expected state, but not a change in the isLoading value. This can be tested either by starting with an isLoading value of true or by writing an additional test that captures the functionality.

9.7. Testing selectors

Testing selectors is nearly as straightforward as testing reducers. Similar to reducers, selectors can be pure functions. However, selectors created with reselect have additional functionality, namely memoization. Listing 9.16 introduces a couple of generic selectors and one selector created with reselect. Testing the first two generic selectors looks like a test for any other pure function. Given input, they should always produce the same output. The standard test function signature will do. The code is taken directly from the initial task search feature implementation.

Listing 9.16. Example selectors
import { createSelector } from 'reselect';

export const getTasks = state => state.tasks.tasks;
export const getSearchTerm = state => state.tasks.searchTerm;            1

export const getFilteredTasks = createSelector(                          2
  [getTasks, getSearchTerm],                                             3
  (tasks, searchTerm) => {
    return tasks.filter(task => task.title.match(new RegExp(searchTerm,
 'i')));                                                               4
  }
);

  • 1 Exports the generic selectors
  • 2 Exports the reselect selector
  • 3 The selector relies on the results of other selectors.
  • 4 Returns the filtered list of tasks

Selectors written with reselect get more interesting, so reselect provides additional helper functions. Besides returning the desired output, another key piece of functionality worth testing is that the function is memoizing the correct data and limiting the number of recalculations it performs. A helper method, recomputations, is available for this purpose. Again, generic selectors take a state as input and produce a slice of the state as output. reselect selectors perform a similar task but avoid extra work if they can help it. If the getFilteredTasks function has the same inputs, it won’t bother recalculating the output, opting instead to return the last stored output.

In between tests, you can reset the number to zero by using the resetRecomputations method, as shown in the following listing.

Listing 9.17. Testing selectors
import { getTasks, getSearchTerm, getFilteredTasks } from '../reducers/';
import cloneDeep from 'lodash/cloneDeep';

describe('tasks selectors', () => {
  const state = {                                                          1
    tasks: {
      tasks: [
        { title: 'Test selectors', description: 'Very meta' },
        { title: 'Learn Redux', description: 'Oh my!' },
      ],
      searchTerm: 'red',
      isLoading: false,
      error: null,
    },
  };

  afterEach(() => {                                                        2
    getFilteredTasks.resetRecomputations();                                3
  });

  it('should retrieve tasks from the getTasks selector', () => {           4
    expect(getTasks(state)).toEqual(state.tasks.tasks);
  });

  it('should retrieve the searchTerm from the getSearchTerm selector', ()
 => {
    expect(getSearchTerm(state)).toEqual(state.tasks.searchTerm);
  });

  it('should return tasks from the getFilteredTasks selector', () => {
    const expectedTasks = [{ title: 'Learn Redux', description: 'Oh my!'
 }];

    expect(getFilteredTasks(state)).toEqual(expectedTasks);
  });

  it('should minimally recompute the state when getFilteredTasks is
 called', () => {
    const similarSearch = cloneDeep(state);                                5
    similarSearch.tasks.searchTerm = 'redu';

    const uniqueSearch = cloneDeep(state);
    uniqueSearch.tasks.searchTerm = 'selec';

    expect(getFilteredTasks.recomputations()).toEqual(0);
    getFilteredTasks(state);
    getFilteredTasks(similarSearch);
    expect(getFilteredTasks.recomputations()).toEqual(1);                  6
    getFilteredTasks(uniqueSearch);
    expect(getFilteredTasks.recomputations()).toEqual(2);
  });
});

  • 1 Builds state tree for use by multiple tests
  • 2 The afterEach function executes a callback after each test.
  • 3 Cleans the test environ-ment after each test
  • 4 Generic selectors are tested like any other pure function.
  • 5 Prepares new state versions to test selector output
  • 6 Verifies the selector does the minimum number of recomputations

Selectors are generally tested in dedicated files or alongside reducers. Having access to a reducer function and using it to produce the next state for your selector to recompute can make for a great integration test. You get to decide how granular of a unit test you’d like.

9.8. Testing components

Now, because we discourage TDD with components doesn’t mean we’re suggesting not testing them. Component tests are valuable for detecting breaking changes to the UI when state is added or removed. This section is broken into a couple of parts: testing presentational components and testing container components.

Again, these tests will make use of the testing tool, Enzyme. If you’ve any prior React experience, there’s a fair chance you’ve used Enzyme to test common React components already. Enzyme was written to facilitate React testing; however, adding Redux to the mix doesn’t change the equation. You can use still use Enzyme to test presentational and container components.

9.8.1. Testing presentational components

Presentational components don’t have access to the Redux store. They’re standard React components; they accept props, manage local component state, and render DOM elements. Recall that presentational components can either be stateless or stateful. For this first example, you’ll examine a stateless presentational component, often referred to as a functional stateless component.

The following listing shows the TaskList component. It accepts props, then renders a status and an array of Task components.

Listing 9.18. Example presentational component
import React from 'react';
import Task from '../components/Task';

const TaskList = props => {                                               1
  return (
    <div className="task-list">
      <div className="task-list-title">
        <strong>{props.status}</strong>                                   2
      </div>
      {props.tasks.map(task => (                                          3
        <Task key={task.id} task={task} onStatusChange={props.onStatusChange} />
      ))}
    </div>
  );
}

export default TaskList;                                                  4

  • 1 The component accepts props from a parent component.
  • 2 Renders a status prop
  • 3 Renders a Task component for each task prop
  • 4 Exports the component for usage and testing

Testing this type of component is a matter of expecting the important pieces to get rendered to the DOM. Listing 9.19 demonstrates some of the types of things you can test for. The couple examples included check for the text value of a particular element and the number of expected elements rendered as a result of a prop value.

We have an implementation detail worth calling out here. As mentioned in the introduction, Enzyme version 3 introduces the concept of Adapters. Airbnb maintains an adapter for each of the latest versions of React, so you’ll configure Enzyme with the React 16 adapter. These adapters are separate installations, so in your case you’ll use the package enzyme-adapter-react-16.

To traverse the component and make assertions, you need Enzyme to virtually mount the component. The two methods most frequently used to accomplish this are Enzyme’s shallow and mount. Unlike shallow, mount renders all children components, allows for simulating events (for example, click), and can be used to test React lifecycle callbacks (for example, componentDidMount). As a rule of thumb, use shallow until your test requires the extra functionality provided by mount, for the sake of performance. Your first examples use shallow, as shown in listing 9.19

Components mounted with Enzyme are commonly saved to a variable called wrapper. This is a convention perpetuated by examples in the official documentation. Note that the find method can locate more than element types. Elements can be selected using class names and IDs, similar to CSS selectors: wrapper.find('.className') or wrapper.find('#id').

Listing 9.19. Presentational component tests
import React from 'react';
import Enzyme, { shallow } from 'enzyme';                                     1
import Adapter from 'enzyme-adapter-react-16';
import TaskList from '../components/TaskList';                                2

Enzyme.configure({ adapter: new Adapter() });

describe('the TaskList component', () => {
  it('should render a status', () => {
    const wrapper = shallow(<TaskList status="In Progress" tasks={[]} />);    3

    expect(wrapper.find('strong').text()).toEqual('In Progress');             4
  });

  it('should render a Task component for each task', () => {
    const tasks = [
      { id: 1, title: 'A', description: 'a', status: 'Unstarted', timer: 0 [CA]},
      { id: 2, title: 'B', description: 'b', status: 'Unstarted', timer: 0 [CA]},
      { id: 3, title: 'C', description: 'c', status: 'Unstarted', timer: 0 [CA]}
    ]
    const wrapper = shallow(<TaskList status="Unstarted" tasks={tasks} />);

    expect(wrapper.find('Task').length).toEqual(3);                           5
  });
});

  • 1 Imports shallow from enzyme to mount the component
  • 2 Imports the component to test
  • 3 Uses Enzyme to mount the component for testing
  • 4 Asserts that the DOM contains the right text values
  • 5 Uses Enzyme to find the number of expected elements

For a long list of additional API methods available to the wrapper, reference the documentation at http://airbnb.io/enzyme/docs/api/shallow.html.

9.8.2. Snapshot testing

As mentioned in the chapter introduction, Jest comes with a powerful feature called snapshot testing. Snapshot testing can be used to catch any change to your UI at a granular level. It works by capturing a JSON representation of your component the first time the test runs; then each subsequent run of the test compares a fresh rendering of the component to the saved snapshot. If anything in the component has changed since the snapshot, you have the opportunity to either fix the unintentional change or confirm that the update is what you wanted. If the change was desired, the saved snapshot will update.

Snapshot testing diverges in several ways from existing test paradigms. It isn’t possible to perform TDD with snapshots. By definition, the component must already be written for the first snapshot to be recorded. This concept jives with our recommendation to test-drive the development of everything except the components.

You’ll find that snapshot testing introduces a slightly new workflow, as well. As you’ll soon see, writing snapshot tests is tremendously easy and requires nearly zero effort upfront. As such, the speed at which you write tests can accelerate. The other side of the coin is that these new tests provide much output you won’t be used to handling. For each tweak you make to the UI, the test suite asks about it. This has the potential to feel obnoxious or overwhelming, until it demonstrates real value and becomes part of the normal workflow.

Jest’s well-designed watch mode is the key to making snapshot testing a breeze. If you want to update a snapshot to the most recently tested version of a component, you can do so with a single keystroke. The “u” key will update the saved snapshot, and you’re free to carry on. You’ll see an example of this output shortly.

One extra package exists that you’ll want to leverage to make snapshot testing a seamless process. Install enzyme-to-json in your devDependencies by running the following command:

npm install -D enzyme-to-json

This does exactly what the name implies: converts the component, mounted with Enzyme, to JSON. Without this transform, the component JSON will always include dynamic debugging values, making snapshots fail when they shouldn’t.

You’re ready to write a snapshot test. You’ll continue to use the TaskList component from the previous section as our example. In the same describe block, you’ll add the snapshot test seen in listing 9.20.

As mentioned, these tests are exceptionally simple to write. Once you’ve mounted the component, you’ll expect the JSON-converted version to match the saved snapshot, using the toMatchSnapshot method. The first time toMatchSnapshot is called, a new snapshot will be saved. Each time thereafter, the component will be compared to the last saved snapshot.

Listing 9.20. Example snapshot tests
...
import toJson from 'enzyme-to-json';                                         1

describe('the TaskList component', () => {
  ...

  it('should match the last snapshot without tasks', () => {
    const wrapper = shallow(<TaskList status="In Progress" tasks={[]} />);
    expect(toJson(wrapper)).toMatchSnapshot();                               2
  });

  it('should match the last snapshot with tasks', () => {                    3
    const tasks = [
      { id: 1, title: 'A', description: 'a', status: 'Unstarted', timer: 0 },
      { id: 2, title: 'B', description: 'b', status: 'Unstarted', timer: 0 },
      { id: 3, title: 'C', description: 'c', status: 'Unstarted', timer: 0 }
    ]
    const wrapper = shallow(<TaskList status="In Progress" tasks={tasks} />);
    expect(toJson(wrapper)).toMatchSnapshot();
  });
});

  • 1 Imports toJson to convert the enzyme wrapper
  • 2 Uses the toMatchSnapshot method to create and compare snapshots
  • 3 You may snapshot test a component multiple times with unique props.

The first time around, Jest will output the following message to the console:

> 2 snapshots written in 1 test suite.

Running the test suite again will produce additional output in the console. You can see that the snapshot tests are running and passing. Figure 9.2 shows the example output.

Figure 9.2. Jest test output with snapshot tests

If you make changes to the UI, you can expect the snapshot tests to let you know about it. Let’s add a class to the strong element that renders the status name within the TaskList component:

<strong className="example">{props.status}</strong>

While the watch mode is running, saving the TaskList file triggers Jest to run the tests again. This time, the snapshot tests will fail, highlighting the differences between the new code and what it expected (figure 9.3).

Figure 9.3. An example snapshot test failure with a file diff of what was received and expected

Further down in the console output, Jest lets you know the outcome: two tests failed, but if the change was intentional, press the “u” key to update the snapshots:

> 2 snapshot tests failed in 1 test suite. Inspect your code changes or press `u` to update them.

If you like the changes, you can hit the “u” key and watch the test suite run again. In the results, all tests are now passing, and you get a confirmation that the snapshots were updated:

> 2 snapshots updated in 1 test suite.

Snapshot testing with Jest can give you a high degree of confidence that your UI isn’t changing unexpectedly. As a rule of thumb, you should use snapshots to unit test individual components. Snapshot tests that encompass the entire application aren’t that valuable, because failures will come from distant corners of the app, be exceptionally noisy, and require frequent updates to the snapshots.

The workflow will likely feel unnatural at first. Give yourself time to get a feel for how many snapshot tests are appropriate and how to integrate them into your workflow. Given how easy they are to write, you may have to resist the urge to go overboard with snapshots. Over time, you’ll find the right balance.

Note that although Jest was the first framework to introduce and popularize snapshot testing, the functionality isn’t exclusive to it. Other frameworks, such as Ava, have since adopted the feature. Remember that the key to snapshots’ ease of use is a robust test runner that accommodates them. At the time of writing, Jest is ahead of the pack there, but it may not be long before others catch up and offer similar quality.

9.8.3. Testing container components

Container components are components with access to the Redux store. You know that you’re working with a container component when you see the connect method, from react-redux, wrapping the component prior to export.

Testing a container component requires accounting for a couple of extra concerns beyond normal components. First, the component will have additional props available to it, between the dispatch method and any additional props specified within the mapStateToProps function. The second and more tedious concern is the fact that you’re exporting not the component you wrote, but instead an enhanced component. Let’s examine these ideas using an example.

The following listing contains the App component in its state at the resolution of chapter 7. The component is enhanced using the connect method. On mount, it uses the dispatch function to send an action to the store.

Listing 9.21. Example container component
import React, { Component } from 'react';
import Enzyme, { connect } from 'react-redux';             1
import Adapter from 'enzyme-adapter-react-16';
import TasksPage from './components/TasksPage';
import FlashMessage from './components/FlashMessage';
import { createTask, editTask, fetchTasks, filterTasks } from './actions';
import { getGroupedAndFilteredTasks } from './reducers/';

Enzyme.configure({ adapter: new Adapter() });

class App extends Component {
  componentDidMount() {
    this.props.dispatch(fetchTasks());                     2
  }

  onCreateTask = ({ title, description }) => {
    this.props.dispatch(createTask({ title, description }));
  };

  onStatusChange = (id, status) => {
    this.props.dispatch(editTask(id, { status }));
  };

  onSearch = searchTerm => {
    this.props.dispatch(filterTasks(searchTerm));
  };

  render() {
    return (
      <div className="container">
        {this.props.error && <FlashMessage message={this.props.error} />}
        <div className="main-content">
          <TasksPage
            tasks={this.props.tasks}
            onCreateTask={this.onCreateTask}
            onSearch={this.onSearch}
            onStatusChange={this.onStatusChange}
            isLoading={this.props.isLoading}
          />
        </div>
      </div>
    );
  }
}

function mapStateToProps(state) {
  const { isLoading, error } = state.tasks;

  return {                                             3
    tasks: getGroupedAndFilteredTasks(state),
    isLoading,
    error
  };
}

export default connect(mapStateToProps)(App);          4

  • 1 The connect method enables a component to access the Redux store.
  • 2 When the component mounts, an action creator is invoked.
  • 3 Details which slices of Redux store state the component can read
  • 4 Uses connect to enhance the App component

Let’s try writing the first test. When the Redux store contains an error in the tasks key, you want to render a FlashMessage component with that error message. If you try to test this component the way you tested the presentational component, you’ll see that you quickly find trouble. Let’s step through it.

The following listing attempts a simple test. You mount the App component, provide an error prop, and make an assertion that the FlashMessage component is rendered.

Listing 9.22. A container test attempt
import React from 'react';
import { shallow } from 'enzyme';
import App from '../App';                                                  1

describe('the App container', () => {
  it('should render a FlashMessage component if there is an error', () => {
    const wrapper = shallow(<App error="Boom!" />);                        2
    expect(wrapper.find('FlashMessage').exists()).toBe(true);              3
  });
});

  • 1 Imports the component for testing
  • 2 Mounts the component with an error prop
  • 3 Asserts that the error message renders

Running this test produces an interesting and descriptive error message:

Invariant Violation: Could not find "store" in either the context or props of
"Connect(App)". Either wrap the root component in a <Provider>, or explicitly
pass "store" as a prop to "Connect(App)".

This error message reveals much about where you’ve gone wrong so far. Notably, you’re referencing an App component, but what you imported is an enhanced component, Connect(App). Under the hood, that enhanced component expects to interact with a Redux store instance, and when it doesn’t find one, it throws an error.

You have at least a couple of ways you can account for the error messages. The easiest route is to export the unconnected App component, in addition to the connected one. All this requires is adding the export keyword to the class declaration, as follows:

export class App extends Component {

Once you’ve exported the unconnected App component, you can then import either the enhanced or the plain component. The enhanced component is the default export, so when you want to use it, you can use the default import syntax, the way you already do in the example test. If you want to import the unconnected component, you’ll need to specify that using curly braces:

import { App } from '../App';

With these changes to the component and test file, the test suite should pass. You’re free to test the App component as if it weren’t connected to Redux anymore, because, of course, it isn’t. If you want to test any props that App should receive from the Redux store, you’ll have to pass those in yourself, the same way as the error prop in the previous example.

Let’s add another test of the unconnected component. Listing 9.23 adds a test to verify that the dispatch method is executed when the component mounts. Recall that Enzyme’s mount function is required to test React lifecycle methods, such as componentDidMount. You need to import mount and use it instead of shallow for this test.

Jest offers a simple way to create spies, using jest.fn(). Spies can keep track of whether they’ve been executed, so you’ll use one to verify that dispatch is called. Jest supplies the toHaveBeenCalled assertion method for this purpose. You’ll notice that you’ve also added an empty array of tasks to the component to prevent a rendering error in the component.

Listing 9.23. Example container test
import React from 'react';
import { shallow, mount } from 'enzyme';                                   1
import { App } from '../App';                                              2

describe('the App container', () => {
  ...
  it('should dispatch fetchTasks on mount', () => {
    const spy = jest.fn();                                                  3
    const wrapper = mount(<App dispatch={spy} error="Boom!" tasks={[]} />); 4

    expect(spy).toHaveBeenCalled();                                         5
  });
});

  • 1 Adds mount to the enzyme imports
  • 2 Imports the unconnected component
  • 3 Creates a spy
  • 4 Adds dispatch and tasks props
  • 5 Asserts that the function was called

You find that testing container components in this fashion is usually sufficient for the test coverage you want. However, there may be times that you’re interested in testing the Redux functionality as well. The way to accomplish that is to wrap the container in a Provider component with a store instance.

Needing a store instance is a common enough need to warrant a utility package. Install redux-mock-store to your devDependencies with the following command:

npm install -D redux-mock-store

You have the rest of the necessary dependencies already installed, so write the test. Listing 9.24 puts it all together to test that the FETCH_TASKS_STARTED action is dispatched on mount. A few new imports are required: the Provider component, the configureMockStore method, any middleware you use in the application, and the connected component.

In the unit test, you’ll want to mount the connected component, wrapped by the Provider component. Similar to the actual implementation, the Provider component needs a store instance passed in as a prop. To create the store instance, you need to provide the middleware and an initial store state.

Once the mock store is initialized, you can use it to view any actions that it receives. The mock store keeps track of an array of dispatched actions and can be used to make assertions about the content and number of those actions.

Listing 9.24. Container test with a mock store
import React from 'react';
import { shallow, mount } from 'enzyme';
import { Provider } from 'react-redux';                                 1
import configureMockStore from 'redux-mock-store';                      1
import thunk from 'redux-thunk';                                        1
import ConnectedApp, { App } from '../App';                             1

describe('the App container', () => {
  ...
  it('should fetch tasks on mount', () => {
    const middlewares = [thunk];                                        2
    const initialState = {                                              3
      tasks: {
        tasks: [],
        isLoading: false,
        error: null,
        searchTerm: '',
      },
    };
    const mockStore = configureMockStore(middlewares)(initialState);    4
    const wrapper = mount(<Provider store={mockStore}><ConnectedApp
 /></Provider>);                                                      5
    const expectedAction = { type: 'FETCH_TASKS_STARTED' };

    expect(mockStore.getActions()[0]).toEqual(expectedAction);          6
  });
});

  • 1 Adds the new imports to wrap the connected component
  • 2 Adds any middleware used in your app
  • 3 Creates an initial state for the store
  • 4 Creates the mock store
  • 5 Mounts the wrapped container component
  • 6 Tests that the action was dispatched to the mock store

Although this strategy is definitely more labor-intensive than testing the unconnected component, you get something for the extra effort. This flavor of test integrates more pieces of the puzzle, and therefore makes for a higher-quality integration test.

Tip

We recommend you write most component unit tests in the unconnected fashion and write a smaller number of integration-style tests using mock stores.

Before we move on from component testing, there’s one debugging strategy we can’t leave out. If you’re ever stumped as to why a test is passing or failing unexpectedly, you can log the contents of a component to the console. The debug method allows you to visualize each element in the virtual DOM:

console.log(wrapper.debug());

Use this line within any test to compare the actual DOM elements with your expectations. More often than not, this will keep you moving forward.

9.9. Exercise

To practice testing each component, you can write additional tests for any of Parsnip’s functionality that we haven’t covered so far in the chapter. Use this chapter’s examples as a guide and adjust each individual test to fit your needs.

For now, try writing another async action test, this time for the editTask action creator. The following listing shows a simple implementation of editTask you can work from so we’re all on the same page.

Listing 9.25. The editTask action creator
function editTaskStarted() {
  return {
    type: 'EDIT_TASK_STARTED',
  };
}

function editTaskSucceeded(task) {
  return {
    type: 'EDIT_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

export function editTask(task, params = {}) {
  return (dispatch, getState) => {
    dispatch(editTaskStarted());

    return api.editTask(task.id, params).then(resp => {
      dispatch(editTaskSucceeded(resp.data));
    });
  };
}

Given that editTask is a standard request-based async action creator, here are a few good things to test:

  • The EDIT_TASK_STARTED action is dispatched.
  • The API call is made with the correct arguments.
  • The EDIT_TASK_SUCCEEDED action is dispatched.

9.10. Solution

Here’s how to approach this:

  • Create the mock store with redux-thunk middleware.
  • Mock the api.editTask function.
  • Create the mock store.
  • Dispatch the action creator, editTask.
  • Assert that the right actions are dispatched by editTask, and that the request is made.

Keep in mind this is much like the test for createTask you wrote earlier in the chapter. Generally, a testing strategy like this will work well with any thunk-based async action. The following listing shows the solution.

Listing 9.26. Testing the editTask action creator
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { editTask } from './';                                             1

jest.unmock('../api');                                                     2
import * as api from '../api';                                             2
api.editTask = jest.fn(() => new Promise((resolve, reject) => resolve({    2
 data: 'foo' })));                                                       2

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);                         3

describe('editTask', () => {
  it('dispatches the right actions', () => {
    const expectedActions = [                                              4
      { type: 'EDIT_TASK_STARTED' },
      { type: 'EDIT_TASK_SUCCEEDED', payload: { task: 'foo' } }
    ];

    const store = mockStore({ tasks: {                                     5
      tasks: [
        {id: 1, title: 'old' }
      ]
    }});

    return store.dispatch(editTask({id: 1, title: 'old'}, { title: 'new'
 })).then(() => {                                                        6
      expect(store.getActions()).toEqual(expectedActions);                 7
      expect(api.editTask).toHaveBeenCalledWith(1, { title: 'new' });      8
    });
  });
});

  • 1 Imports the async action you want to test
  • 2 Mocks the API call with a resolved promise
  • 3 Adds the redux-thunk middleware to the mock store
  • 4 Creates a list of expected actions you’ll use in an assertion later in the test
  • 5 Creates the mock store
  • 6 Dispatches the editTask action
  • 7 Asserts the API request is made with the correct arguments
  • 8 Asserts the correct actions are dispatched

Of note, you’re continuing to mock the api.editTask method directly, meaning you totally bypass any test setup related to handling HTTP requests. HTTP-mocking libraries such as nock are always an option and may give you more bang for your buck, as you can also exercise making the HTTP request.

JavaScript developers have left code untested for years! Why start now? That’s a joke, of course, but testing JavaScript has earned a reputation for been notoriously difficult. That difficulty largely stems from the deeply coupled, “spaghetti” code that too often characterized client-side applications of the past. Without a robust test suite, however, it’s difficult to have any confidence that new code isn’t introducing breaking changes.

Fortunately, Redux moves us a large step toward sanity by decoupling many of the moving pieces into small, testable units. Are there still confusing parts? You bet. Are we headed in the right direction, though? We sure think so.

In the next chapter, we’ll move on to discuss topics related to performance in Redux. You’ll find several ideas for how to optimize React and Redux applications for scale.

Summary

  • Jest builds upon Jasmine’s test runner and assertion library and comes standard within apps generated by Create React App.
  • Testing pure functions, such as reducers or synchronous action creators, is simplest.
  • Testing sagas is a matter of testing the effect object that gets sent to the middleware.
  • The two ways to test connected components are to import and wrap a Provider component or to export the unconnected component.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.96.232