0

I've been using the following pattern to test conditions after state updates in my components using react.

await expect(new Promise((resolve) => {
  let intervalId = setInterval(() => {
    component.update();

    if (myCondition) {
      clearInterval(intervalId);

      resolve(true);
    }
  }, 25);
})).resolves.toBe(true);

This works well and is guaranteed to work but it's a pain to write and quite verbose.

I've been looking into possibly using setImmediate rather than setInterval. This would prevent the polling (and allows me to test negative assertions which currently aren't possible without introducing another level of verbosity with a try/catch), but is it guranteed to work with react async mechanisms such as setState?

For example what happens if react decides to batch try to batch some setState events together or something along those lines and setImmediate gets put in the event loop queue before react dispatches the state update actions?

I don't want to introduce flakiness to my tests.

Canta
  • 1,480
  • 1
  • 13
  • 26
Adam Thompson
  • 3,278
  • 3
  • 22
  • 37
  • From MDN: `Warning: Non-standard This feature is non-standard and is not on a standards track. Do not use it on production sites facing the Web: it will not work for every user. There may also be large incompatibilities between implementations and the behavior may change in the future.`. Doesn't sound promising. – CertainPerformance Aug 25 '18 at 02:13
  • 1
    It is flaky. That you need such tricks usually means that either your component or your test design is flawed. Consider providing the code for a component and a test if you're interested in better solution. – Estus Flask Aug 25 '18 at 02:14
  • 1
    @CertainPerformance It's Jest question, i.e. this is Node. – Estus Flask Aug 25 '18 at 02:14
  • @estus I've already moved up all my async logic into container components in order to make most of my main components way more testable but I still need to test the containers and make sure they are fetching and passing down the right data. I'm not setting the state directly, rather testing the behaviour of components which _happens_ to involve an async call to the backend API. – Adam Thompson Aug 25 '18 at 02:22
  • The question doesn't say this explicitly but this is integration/e2e test. Jest basic functionality is primarily intended for unit testing. For e2e tests consider waiting for DOM elements, etc, e.g. https://github.com/kentcdodds/dom-testing-library . Waiting for DOM elements involves polling. You can't guarantee that some asynchronous process completes within fixed amount of time, especially since you're focusing on behaviour. *it's a pain to write and quite verbose* - you can write helper function once or use somebody's else. – Estus Flask Aug 25 '18 at 02:34
  • As for async setState alone, I'd expect it to be not longer than 1 tick, so zero setTimeout or `await null` may be enough. I'm not sure about setImmediate because faster isn't better in this case. – Estus Flask Aug 25 '18 at 02:38
  • @estus I'm not sure I would classify it as e2e because I'm mocking out the API in my case, and even with the API I could be testing the same thing with or without the API call. The point is to test some behaviour that happens to involve setting the state of a component and updating some part of its _contract_ which is at the seem of the component and should be tested IMO. I'm not sure that setTimeout with 0 is enough because react might wait to put the setState events on the callback queue (since it's mentioned in the react docs that setStates might get batched.) – Adam Thompson Aug 25 '18 at 02:44
  • 1
    Even if it's integration test, without API calls you can make it entirely synchronous with https://jestjs.io/docs/en/timer-mocks.html , or chain promises from mocked responses, similarly to this unit test, https://stackoverflow.com/a/52013374/3731501 . I didn't examined React internals that close but I expect the queue to be finished asynchronously but within same tick. I suppose the thing you're looking for is `ReactDOM.flushSync`. You could promisify it like `await util.promisify(flushSync)()`. – Estus Flask Aug 25 '18 at 03:08
  • Does the first approach rely on react using timers to implement their setState? I'm not sure if they use timers? Is mocking timers and then running them to completion a guarantee that react any setStates will be completed? The second approach would require converting any components being tested to use dependency injection, which I'm not too keen on doing just to make it more testable (though maybe I'm wrong on that mindset!) `flushSync` looks promising, I'll dig deeper, though I would be suprised its not more ubiquitious in testing as it would help a ton if it forces all setStates to complete. – Adam Thompson Aug 25 '18 at 03:28
  • Yes, all async behaviour is caused by async APIs. Timer functions are addressed with Jest, so I'd expect this will work for setState. Async behaviour caused by promises can be addressed with https://www.npmjs.com/package/jest-mock-promise but this can affect how things work, proceed with care. I'm not sure DI is needed, keeping promises to chain as private properties in stateful components could be enough. – Estus Flask Aug 25 '18 at 13:23

0 Answers0