简体   繁体   中英

How to test a React component that dispatches a Redux / Thunk action

I'm writing an integration test for a component that should redirect to a specific path depending on the response from an asynchronous (thunk) redux action.

This is a simplified version of my component:

class MyComponent extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      redirect: false
    }

    this.props.dispatch(asyncThunkAction())
      .then( () => this.setState({redirec: true}) )
      .catch( (err) => console.log('action failed') )
  }

 ...

  render() {

    if (this.state.redirect) {
      return <Redirect to='/whocares' />
    }
    ...
  }
}

function mapStateToProps(state) {
  return {
     ...
  };
}

export default connect(mapStateToProps)(MyComponent);

I want to write a test that asserts that the component redirected to the expected path.

I am using this technique for inspecting the actual redirection path (It's not perfect but it's not the focus of this question).

The place where I am stuck is the state change in the .then() following the redux/thunk action. Because it's a promise, the redirect happens after my expect statement, so I have not been able to test that.

Here's what my test looks like:

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

  test('redirects after thunk action', () => {
    const redirectUrl = '/whocares'
    const data = {};


    jest.mock('../actions');

    act(() => {
      ReactDOM.render(
        <TestRouter
            ComponentWithRedirection={<MyComponent store={mockStore(data)} />}
            RedirectUrl={redirectUrl}
        />, 
        container);
    });
    expect(container.innerHTML).toEqual(
      expect.stringContaining(redirectUrl)
    )
  })

My TestRouter just prints the anticipated redirect URL into the DOM. (Check out the link above for a full explanation of this hack.) So right now instead of hitting the expected route, my test (correctly) identifies the loading screen that appears while the thunk action is in progress.

I think the right way to do this is to mock the response from asyncThunkAction so that it returns a resolved promise with matching data, but so far I have not been able to figure out how to do that. I followed the Jest documentation on manual mocks and created the corresponding mock file:

// __mocks__/actions.js
const asyncThunkAction = function(){
    return Promise.resolve({foo: 'bar'});
};
export { asyncThunkAction };

...but my test still "sees" the loading state. I don't even think it's looking at my mocked file/action.

What is the right way to do this?

Here's my "recipe" for how I was able to get this working...

Use testing-library/react ...

import { render, fireEvent, waitForElement, act } from '@testing-library/react';

(+1 to @tmahle for this suggestion)

Mock axios (or in my case the API module that wraps it) by creating a " manual mock " which basically entails creating a __mocks__ directory next to the real file containing a file by the same name. Then export an object with a property that replaces the get method (or whichever one your code uses).

//__mocks__/myclient.js
export default {
  get: jest.fn(() => Promise.resolve({ data: {} }))
};

Even if you don't call the mocked code in your test, you need to import it in the test file...

import myapi from '../api/myapi';
jest.mock('../api/myai');

You can mock the response from the mocked API call like this:

myapi.get.mockResolvedValueOnce({
  data: { foo: "bar" },
});

I'm a little fuzzy on this part... Even though the mocked API request responds immediately with a resolved promise, you probably need to wait for it to write expects

const { getByText, getByTestId, container } = render(<MyComponent />);
await wait(() => getByText('Some text that appears after the '));
expect(container.innerHTML).toEqual('whatever');

All of this was "out there" in various docs and SO questions... but it took me a long time to cobble it all together. Hopefully this saves you time.

This is a little bit of a sideways answer to your question, admittedly, but I would recommend trying out testing-library and the ideals that it embodies, especially for integration tests.

It is available in both DOM and React flavors, which one to use likely depends on what level of abstraction your redirect is happening at:

https://github.com/testing-library/dom-testing-library https://github.com/testing-library/react-testing-library

With this paradigm you would not try to assert that the user gets redirected to the correct path, but rather that the correct thing is on the screen after they are redirected. You would also limit your mocking to the absolutely bare necessities (likely nothing or only browser API's that your test environment cannot emulate if you are doing a true integration test).

The overall approach here would probably have you mocking out much less and perhaps rendering a larger portion of the app. A likely-helpful example to draw from can be found here: https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/master/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-router.js&previewwindow=tests

Because there's less mocking in this approach, the specifics for how you can accomplish this would likely come from outside the scope of the example you've given, but the above example link should help a lot with getting started.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM