1

I'm trying to mock a slow network in Jest (with React testing library). The condition I'm trying to set up is something like this:

  1. UI is in a specific state
  2. async AJAX request is fired off
  3. ui changes state
  4. async request completes

It seems to me this would be quite a common requirement, because with asynchronous programming you have to be aware of what might have happened between the AJAX request being sent and when it's completed. However, I can't find any discussion or tools in this area at all.

Andy
  • 10,412
  • 13
  • 70
  • 95

1 Answers1

5

This reply is quite specific to react-testing-libaray but I think the concept is generally applicable.

First of all I realised that if you start the async operation in response to a user event (say a click) then you don't have a problem - as soon as you've called fireEvent.click, you are in state 3, then you can use waitFor() to wait until you get to state 4.

Secondly, if your environment gives you a handle to the promise for the async operation then of course you can just wait for that so again you don't have a problem.

In my case, the async operation was fired off by a timer, and because it was inside a react component, I get a handle to that timer from the outside. So my scenario is something like this:

export default class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = { message: "initial" };
    }
    componentDidMount = () => setTimeout(async () => {
        this.setState({ message: "loading" });
        var message = await this.props.longRunningOperation();
        this.setState({ message });
    }, 5000);
    render = () => <div>{this.state.message}</div>
}

The component state is initially "initial", 5 seconds after it mounts, it fires off the long running operation and sets the state to "loading", then when it gets a response, it sets the state to the text of that response. My test case looks like this:

test("long running response", async () => {
    const fakeLongRunningOperation = async () => new Promise(resolve =>
              setTimeout(() => resolve("success"), 2000));
    jest.useFakeTimers();
    var app = render(<App longRunningOperation={fakeLongRunningOperation} />);
    jest.advanceTimersByTime(4000)
    expect(app.queryByText("initial")).not.toBeNull();
    jest.advanceTimersByTime(2000)
    expect(app.queryByText("loading")).not.toBeNull();
    jest.advanceTimersByTime(2000)
    await waitFor(() => { expect(app.queryByText("success")).not.toBeNull() });
});

We fake the long running operation with a function that returns "success" after a 2 second delay. After 4 seconds, we expect the state to still be "initial", then after another 2 seconds we expect it to be "loading" and after another 2 seconds, we expect it to be "success". And we use jest fake timers so the test doesn't actually take 8 seconds to run.

Hope that helps anyone else who is trying to do a similar kind of thing.

Robin Métral
  • 3,099
  • 3
  • 17
  • 32
Andy
  • 10,412
  • 13
  • 70
  • 95