Skip to content

Commit 01d319c

Browse files
docs: async tests [Cookbook] (#1631)
* wip: async tests * docs: tweaks * docs: final tweaks
1 parent 723c8e2 commit 01d319c

File tree

3 files changed

+147
-5
lines changed

3 files changed

+147
-5
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
["custom-render"]
1+
["async-tests", "custom-render"]
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Async tests
2+
3+
## Summary
4+
5+
Typically, you would write synchronous tests, as they are simple and get the work done. However, there are cases when using asynchronous (async) tests might be necessary or beneficial. The two most common cases are:
6+
7+
1. **Testing Code with asynchronous operations**: When your code relies on asynchronous operations, such as network calls or database queries, async tests are essential. Even though you should mock these network calls, the mock should act similarly to the actual behavior and hence by async.
8+
2. **UserEvent API:** Using the [User Event API](docs/api/events/user-event) in your tests creates more realistic event handling. These interactions introduce delays (even though these are typically event-loop ticks with 0 ms delays), requiring async tests to handle the timing correctly.
9+
10+
Using async tests when needed ensures your tests are reliable and simulate real-world conditions accurately.
11+
12+
### Example
13+
14+
Consider a basic asynchronous test for a user signing in with correct credentials:
15+
16+
```javascript
17+
test('User can sign in with correct credentials', async () => {
18+
// Typical test setup
19+
const user = userEvent.setup();
20+
render(<App />);
21+
22+
// No need to use async here, components are already rendered
23+
expect(screen.getByRole('header', { name: 'Sign in to Hello World App!' })).toBeOnTheScreen();
24+
25+
// Using await as User Event requires it
26+
await user.type(screen.getByLabelText('Username'), 'admin');
27+
await user.type(screen.getByLabelText('Password'), 'admin1');
28+
await user.press(screen.getByRole('button', { name: 'Sign In' }));
29+
30+
// Using await as sign in operation is asynchronous
31+
expect(await screen.findByRole('header', { name: 'Welcome admin!' })).toBeOnTheScreen();
32+
33+
// Follow-up assertions do not need to be async, as we already waited for sign in operation to complete
34+
expect(
35+
screen.queryByRole('header', { name: 'Sign in to Hello World App' })
36+
).not.toBeOnTheScreen();
37+
expect(screen.queryByLabelText('Username')).not.toBeOnTheScreen();
38+
expect(screen.queryByLabelText('Password')).not.toBeOnTheScreen();
39+
});
40+
```
41+
42+
## Async utilities
43+
44+
There are several asynchronous utilities you might use in your tests.
45+
46+
### `findBy*` queries
47+
48+
The most common are the [`findBy*` queries](docs/api/queries#find-by). These are useful when waiting for a matching element to appear. They can be understood as a [`getBy*` queries](docs/api/queries#get-by) used in conjunction with a [`waitFor` function](docs/api/misc/async#waitfor).
49+
50+
They accept the same predicates as `getBy*` queries like `findByRole`, `findByTest`, etc. They also have a multiple elements variant called [`findAllBy*`](docs/api/queries#find-all-by).
51+
52+
```typescript
53+
function findByRole: (
54+
role: TextMatch,
55+
queryOptions?: {
56+
// Query specific options
57+
}
58+
waitForOptions?: {
59+
timeout?: number;
60+
interval?: number;
61+
// ..
62+
}
63+
): Promise<ReactTestInstance>;
64+
```
65+
66+
Each query has a default `timeout` value of 1000 ms and a default `interval` of 50 ms. Custom timeout and check intervals can be specified if needed, as shown below:
67+
68+
#### Example
69+
70+
```typescript
71+
const button = await screen.findByRole('button'), { name: 'Start' }, { timeout: 1000, interval: 50 });
72+
```
73+
74+
Alternatively, a default global `timeout` value can be set using the [`configure` function](docs/api/misc/config#configure):
75+
76+
```typescript
77+
configure({ asyncUtilTimeout: timeout });
78+
```
79+
80+
### `waitFor` function
81+
82+
The `waitFor` function is another option, serving as a lower-level utility in more advanced cases.
83+
84+
```typescript
85+
function waitFor<T>(
86+
expectation: () => T,
87+
options?: {
88+
timeout: number;
89+
interval: number;
90+
}
91+
): Promise<T>;
92+
```
93+
94+
It accepts an `expectation` to be validated and repeats the check every defined interval until it no longer throws an error. Similarly to `findBy*` queries they accept `timeout` and `interval` options and have the same default values of 1000ms for timeout, and a checking interval of 50 ms.
95+
96+
#### Example
97+
98+
```typescript
99+
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1));
100+
```
101+
102+
If you want to use it with `getBy*` queries, use the `findBy*` queries instead, as they essentially do the same, but offer better developer experience.
103+
104+
### `waitForElementToBeRemoved` function
105+
106+
A specialized function, [`waitForElementToBeRemoved`](docs/api/misc/async#waitforelementtoberemoved), is used to verify that a matching element was present but has since been removed.
107+
108+
```typescript
109+
function waitForElementToBeRemoved<T>(
110+
expectation: () => T,
111+
options?: {
112+
timeout: number;
113+
interval: number;
114+
}
115+
): Promise<T> {}
116+
```
117+
118+
This function is, in a way, the negation of `waitFor` as it expects the initial expectation to be true (not throw an error), only to turn invalid (start throwing errors) on subsequent runs. It operates using the same `timeout` and `interval` parameters as `findBy*` queries and `waitFor`.
119+
120+
#### Example
121+
122+
```typescript
123+
await waitForElementToBeRemoved(() => getByText('Hello World'));
124+
```
125+
126+
## Fake Timers
127+
128+
Asynchronous tests can take long to execute due to the delays introduced by asynchronous operations. To mitigate this, fake timers can be used. These are particularly useful when delays are mere waits, such as the 130 milliseconds wait introduced by the UserEvent `press()` event due to React Native runtime behavior or simulated 1000 wait in a API call mock. Fake timers allow for precise fast-forwarding through these wait periods.
129+
130+
Here are the basics of using [Jest fake timers](https://jestjs.io/docs/timer-mocks):
131+
132+
- Enable fake timers with: `jest.useFakeTimers()`
133+
- Disable fake timers with: `jest.useRealTimers()`
134+
- Advance fake timers forward with: `jest.advanceTimersByTime(interval)`
135+
- Run **all timers** to completion with: `jest.runAllTimers()`
136+
- Run **currently pending timers** to completion with: `jest.runOnlyPendingTimers()`
137+
138+
Be cautious when running all timers to completion as it might create an infinite loop if these timers schedule follow-up timers. In such cases, it's safer to use `jest.runOnlyPendingTimers()` to avoid ending up in an infinite loop of scheduled tasks.
139+
140+
You can use both built-in Jest fake timers, as well as [Sinon.JS fake timers](https://sinonjs.org/releases/latest/fake-timers/).
141+
142+
Note: you do not need to advance timers by hand when using User Event API, as it's automatically.

website/docs/12.x/docs/api/misc/async.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ The `findBy*` queries are used to find elements that are not instantly available
99
```tsx
1010
function waitFor<T>(
1111
expectation: () => T,
12-
{ timeout: number = 1000, interval: number = 50 }
13-
): Promise<T> {}
12+
options?: { timeout: number; interval: number }
13+
): Promise<T>;
1414
```
1515

1616
Waits for a period of time for the `expectation` callback to pass. `waitFor` may run the callback a number of times until timeout is reached, as specified by the `timeout` and `interval` options. The callback must throw an error when the expectation is not met. Returning any value, including a falsy one, will be treated as meeting the expectation, and the callback result will be returned to the caller of `waitFor` function.
@@ -109,8 +109,8 @@ If you receive warnings related to `act()` function consult our [Undestanding Ac
109109
```ts
110110
function waitForElementToBeRemoved<T>(
111111
expectation: () => T,
112-
{ timeout: number = 4500, interval: number = 50 }
113-
): Promise<T> {}
112+
options?: { timeout: number; interval: number }
113+
): Promise<T>;
114114
```
115115

116116
Waits for non-deterministic periods of time until queried element is removed or times out. `waitForElementToBeRemoved` periodically calls `expectation` every `interval` milliseconds to determine whether the element has been removed or not.

0 commit comments

Comments
 (0)