From e5cdf1b634ca687325f08ae6480684a9f3da1b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 20 Jun 2024 17:01:27 +0200 Subject: [PATCH 1/3] wip: async tests --- website/docs/12.x/cookbook/basics/_meta.json | 2 +- .../docs/12.x/cookbook/basics/async-tests.md | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 website/docs/12.x/cookbook/basics/async-tests.md diff --git a/website/docs/12.x/cookbook/basics/_meta.json b/website/docs/12.x/cookbook/basics/_meta.json index 895605717..591daedc8 100644 --- a/website/docs/12.x/cookbook/basics/_meta.json +++ b/website/docs/12.x/cookbook/basics/_meta.json @@ -1 +1 @@ -["custom-render"] +["async-tests", "custom-render"] diff --git a/website/docs/12.x/cookbook/basics/async-tests.md b/website/docs/12.x/cookbook/basics/async-tests.md new file mode 100644 index 000000000..fef08001b --- /dev/null +++ b/website/docs/12.x/cookbook/basics/async-tests.md @@ -0,0 +1,70 @@ +# Async tests + +## Summary + +In the context of testing within development, it's important to understand when to use asynchronous tests. Generally, these are necessary when there is some asynchronous event happening in the tested component or hook, or when using UserEvent testing utilities which are asynchronous by nature. + +### Example + +Consider a basic asynchronous test for a user signing in with correct credentials: + +```javascript +test('User can sign in with correct credentials', async () => { + // Typical test setup + const user = userEvent.setup(); + render(); + + // No need to use async here, components are already rendered + expect(screen.getByRole('header', { name: 'Sign in to Hello World App!' })).toBeOnTheScreen(); + + // Using await as User Event requires it + await user.type(screen.getByLabelText('Username'), 'admin'); + await user.type(screen.getByLabelText('Password'), 'admin1'); + await user.press(screen.getByRole('button', { name: 'Sign In' })); + + // Using await as sign in operation is asynchronous + expect(await screen.findByRole('header', { name: 'Welcome admin!' })).toBeOnTheScreen(); + + // Follow-up assertions do not need to be async, as we already waited for sign in operation to complete + expect(screen.queryByRole('header', { name: 'Sign in to Hello World App' })).not.toBeOnTheScreen(); + expect(screen.queryByLabelText('Username')).not.toBeOnTheScreen(); + expect(screen.queryByLabelText('Password')).not.toBeOnTheScreen(); +}); +``` + +## Async utilities + +There are several asynchronous utilities you might use in such tests. + +### `findBy*` queries +One common utility is the `findBy*` queries. These are useful when you need to wait for a matching element to appear and can be summed up as a `getBy*` query within a `waitFor` function. These are widely used due to their efficacy in handling asynchronous elements. They accept the same predicates as `getBy*` queries and have a multiple elements variant called `findAllBy*`. Additionally, custom timeout and check intervals can be specified if needed, as shown below: + +```javascript +await screen.findByText('Hello', {}, { timeout: 1000, interval: 50 }); +``` + +### `waitFor` function +The `waitFor` function is another option, serving as a lower-level utility in more advanced cases. It accepts an expectation to be validated and repeats the check every defined interval until it no longer throws an error. The default interval is 50 milliseconds, and checks continue until a timeout is reached. The global default timeout can be set using the configure option: + +```javascript +configure({ asyncUtilTimeout: timeout }); +``` + +This default timeout is 1000 milliseconds. + +### `waitForElementToBeRemoved` function +A specialized function, waitForElementToBeRemoved, is used to verify that a matching element was present but has since been removed. This function is, in a way, the negation of `waitFor` as it expects the initial expectation to be true, only to turn invalid (throwing an error) on subsequent runs. It operates using the same timeout and interval parameters. + +## Fake Timers +Regarding timers, asynchronous tests can take longer 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 internals. Fake timers allow for fast-forwarding through these wait periods. + +Here are the basics of using fake timers: +- Enable fake timers with: `jest.useFakeTimers();` +- Disable fake timers with: `jest.useRealTimers();` +- Move fake timers forward with: `jest.advanceTimersByTime(interval);` +- Run **all timers** to completion with: `jest.runAllTimers();` +- Run **currently pending timers** to completion with: `jest.runOnlyPendingTimers();` + +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. + +These practices and utilities ensure that asynchronous actions in your tests are handled correctly and efficiently, enabling you to simulate real-world interaction scenarios while keeping test execution times as short as possible. By understanding and appropriately applying these tools, you can create robust and reliable tests for applications that rely on asynchronous operations. From 31e7250bd72259d8edfc1ef15964c0dcef5c31dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 20 Jun 2024 17:24:25 +0200 Subject: [PATCH 2/3] docs: tweaks --- .../docs/12.x/cookbook/basics/async-tests.md | 63 +++++++++++++++---- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/website/docs/12.x/cookbook/basics/async-tests.md b/website/docs/12.x/cookbook/basics/async-tests.md index fef08001b..6c39a54df 100644 --- a/website/docs/12.x/cookbook/basics/async-tests.md +++ b/website/docs/12.x/cookbook/basics/async-tests.md @@ -2,7 +2,11 @@ ## Summary -In the context of testing within development, it's important to understand when to use asynchronous tests. Generally, these are necessary when there is some asynchronous event happening in the tested component or hook, or when using UserEvent testing utilities which are asynchronous by nature. +Typically, you would write synchronous tests. However, there are cases when using asynchronous (async) tests might be necessary or beneficial. The two most common scenarios are: +1. **Testing Code with asynchronous operations**: When your code relies on operations that are asynchronous, such as network calls, async tests are essential. Even though you should mock these network calls, the mock should act similarly to real async behavior. +2. **UserEvent API:** Using the UserEvent API in your tests creates more realistic event handling. These interactions introduce delays (typically event-loop ticks with 0 ms delays), requiring async tests to handle the timing correctly. + +By using async tests when needed, you ensure your tests are reliable and simulate real-world conditions accurately. ### Example @@ -34,29 +38,64 @@ test('User can sign in with correct credentials', async () => { ## Async utilities -There are several asynchronous utilities you might use in such tests. +There are several asynchronous utilities you might use in your tests. ### `findBy*` queries -One common utility is the `findBy*` queries. These are useful when you need to wait for a matching element to appear and can be summed up as a `getBy*` query within a `waitFor` function. These are widely used due to their efficacy in handling asynchronous elements. They accept the same predicates as `getBy*` queries and have a multiple elements variant called `findAllBy*`. Additionally, custom timeout and check intervals can be specified if needed, as shown below: +The most common utility are the `findBy*` queries. These are useful when you need to wait for a matching element to appear. They can be though of as a `getBy*` query used within a `waitFor` helper. + +They accept the same predicates as `getBy*` queries like `findByRole`, `findByTest`, etc. They also have a multiple elements variant called `findAllBy*`. + +```typescript +function findByRole: ( + role: TextMatch, + queryOptions?: { + // Query specific options + } + waitForOptions?: { + timeout?: number; + interval?: number; + // .. + } +): Promise; +``` -```javascript -await screen.findByText('Hello', {}, { timeout: 1000, interval: 50 }); +Each query has a default `timeout` value of 1000 ms and `interval` of 50 ms. Custom timeout and check intervals can be specified if needed, as shown below: + +```typescript +await screen.findByRole('button'), { name: 'Start' }, { timeout: 1000, interval: 50 }); +``` + +Alternatively a default global `timeout` value can be set using `configure()` function: +```typescript +configure({ asyncUtilTimeout: timeout }); ``` ### `waitFor` function -The `waitFor` function is another option, serving as a lower-level utility in more advanced cases. It accepts an expectation to be validated and repeats the check every defined interval until it no longer throws an error. The default interval is 50 milliseconds, and checks continue until a timeout is reached. The global default timeout can be set using the configure option: -```javascript -configure({ asyncUtilTimeout: timeout }); +The `waitFor` function is another option, serving as a lower-level utility in more advanced cases. + +It accepts an `expectation` to be validated and repeats the check every defined interval until it no longer throws an error. The default interval is 50 ms, and checks continue until a timeout is reached. + +It accepts the same `timeout` and `interval` option as `findBy*` queries. + +```typescript +await waitFor(() => getByText('Hello World')); ``` -This default timeout is 1000 milliseconds. +If you want to use it with `getBy*` queries, use the `findBy*` queries instead, as they essentially do the same, but offer better error reporting, etc. + ### `waitForElementToBeRemoved` function -A specialized function, waitForElementToBeRemoved, is used to verify that a matching element was present but has since been removed. This function is, in a way, the negation of `waitFor` as it expects the initial expectation to be true, only to turn invalid (throwing an error) on subsequent runs. It operates using the same timeout and interval parameters. + +A specialized function, `waitForElementToBeRemoved`, is used to verify that a matching element was present but has since been removed. This function is, in a way, the negation of `waitFor` as it expects the initial expectation to be true, only to turn invalid (throwing an error) on subsequent runs. It operates using the same `timeout` and `interval` parameters. + +```typescript +await waitForElementToBeRemoved(() => getByText('Hello World')); +``` ## Fake Timers -Regarding timers, asynchronous tests can take longer 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 internals. Fake timers allow for fast-forwarding through these wait periods. + +Regarding timers, asynchronous tests can take longer 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. Fake timers allow for fast-forwarding through these wait periods. Here are the basics of using fake timers: - Enable fake timers with: `jest.useFakeTimers();` @@ -67,4 +106,4 @@ Here are the basics of using fake timers: 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. -These practices and utilities ensure that asynchronous actions in your tests are handled correctly and efficiently, enabling you to simulate real-world interaction scenarios while keeping test execution times as short as possible. By understanding and appropriately applying these tools, you can create robust and reliable tests for applications that rely on asynchronous operations. +Note: you do not need to advance timers by hand when using User Event, it's automatically. \ No newline at end of file From 59b07de2be81a9308ca94e0656b05245fd56898a Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 21 Jun 2024 10:09:11 +0200 Subject: [PATCH 3/3] docs: final tweaks --- .../docs/12.x/cookbook/basics/async-tests.md | 85 +++++++++++++------ website/docs/12.x/docs/api/misc/async.mdx | 8 +- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/website/docs/12.x/cookbook/basics/async-tests.md b/website/docs/12.x/cookbook/basics/async-tests.md index 6c39a54df..c3900d519 100644 --- a/website/docs/12.x/cookbook/basics/async-tests.md +++ b/website/docs/12.x/cookbook/basics/async-tests.md @@ -2,11 +2,12 @@ ## Summary -Typically, you would write synchronous tests. However, there are cases when using asynchronous (async) tests might be necessary or beneficial. The two most common scenarios are: -1. **Testing Code with asynchronous operations**: When your code relies on operations that are asynchronous, such as network calls, async tests are essential. Even though you should mock these network calls, the mock should act similarly to real async behavior. -2. **UserEvent API:** Using the UserEvent API in your tests creates more realistic event handling. These interactions introduce delays (typically event-loop ticks with 0 ms delays), requiring async tests to handle the timing correctly. +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: -By using async tests when needed, you ensure your tests are reliable and simulate real-world conditions accurately. +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. +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. + +Using async tests when needed ensures your tests are reliable and simulate real-world conditions accurately. ### Example @@ -30,7 +31,9 @@ test('User can sign in with correct credentials', async () => { expect(await screen.findByRole('header', { name: 'Welcome admin!' })).toBeOnTheScreen(); // Follow-up assertions do not need to be async, as we already waited for sign in operation to complete - expect(screen.queryByRole('header', { name: 'Sign in to Hello World App' })).not.toBeOnTheScreen(); + expect( + screen.queryByRole('header', { name: 'Sign in to Hello World App' }) + ).not.toBeOnTheScreen(); expect(screen.queryByLabelText('Username')).not.toBeOnTheScreen(); expect(screen.queryByLabelText('Password')).not.toBeOnTheScreen(); }); @@ -38,12 +41,13 @@ test('User can sign in with correct credentials', async () => { ## Async utilities -There are several asynchronous utilities you might use in your tests. +There are several asynchronous utilities you might use in your tests. ### `findBy*` queries -The most common utility are the `findBy*` queries. These are useful when you need to wait for a matching element to appear. They can be though of as a `getBy*` query used within a `waitFor` helper. -They accept the same predicates as `getBy*` queries like `findByRole`, `findByTest`, etc. They also have a multiple elements variant called `findAllBy*`. +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). + +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). ```typescript function findByRole: ( @@ -59,35 +63,61 @@ function findByRole: ( ): Promise; ``` -Each query has a default `timeout` value of 1000 ms and `interval` of 50 ms. Custom timeout and check intervals can be specified if needed, as shown below: +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: + +#### Example ```typescript -await screen.findByRole('button'), { name: 'Start' }, { timeout: 1000, interval: 50 }); +const button = await screen.findByRole('button'), { name: 'Start' }, { timeout: 1000, interval: 50 }); ``` -Alternatively a default global `timeout` value can be set using `configure()` function: +Alternatively, a default global `timeout` value can be set using the [`configure` function](docs/api/misc/config#configure): + ```typescript configure({ asyncUtilTimeout: timeout }); ``` ### `waitFor` function -The `waitFor` function is another option, serving as a lower-level utility in more advanced cases. +The `waitFor` function is another option, serving as a lower-level utility in more advanced cases. -It accepts an `expectation` to be validated and repeats the check every defined interval until it no longer throws an error. The default interval is 50 ms, and checks continue until a timeout is reached. +```typescript +function waitFor( + expectation: () => T, + options?: { + timeout: number; + interval: number; + } +): Promise; +``` -It accepts the same `timeout` and `interval` option as `findBy*` queries. +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. + +#### Example ```typescript -await waitFor(() => getByText('Hello World')); +await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1)); ``` -If you want to use it with `getBy*` queries, use the `findBy*` queries instead, as they essentially do the same, but offer better error reporting, etc. - +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. ### `waitForElementToBeRemoved` function -A specialized function, `waitForElementToBeRemoved`, is used to verify that a matching element was present but has since been removed. This function is, in a way, the negation of `waitFor` as it expects the initial expectation to be true, only to turn invalid (throwing an error) on subsequent runs. It operates using the same `timeout` and `interval` parameters. +A specialized function, [`waitForElementToBeRemoved`](docs/api/misc/async#waitforelementtoberemoved), is used to verify that a matching element was present but has since been removed. + +```typescript +function waitForElementToBeRemoved( + expectation: () => T, + options?: { + timeout: number; + interval: number; + } +): Promise {} +``` + +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`. + +#### Example ```typescript await waitForElementToBeRemoved(() => getByText('Hello World')); @@ -95,15 +125,18 @@ await waitForElementToBeRemoved(() => getByText('Hello World')); ## Fake Timers -Regarding timers, asynchronous tests can take longer 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. Fake timers allow for fast-forwarding through these wait periods. +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. -Here are the basics of using fake timers: -- Enable fake timers with: `jest.useFakeTimers();` -- Disable fake timers with: `jest.useRealTimers();` -- Move fake timers forward with: `jest.advanceTimersByTime(interval);` -- Run **all timers** to completion with: `jest.runAllTimers();` -- Run **currently pending timers** to completion with: `jest.runOnlyPendingTimers();` +Here are the basics of using [Jest fake timers](https://jestjs.io/docs/timer-mocks): + +- Enable fake timers with: `jest.useFakeTimers()` +- Disable fake timers with: `jest.useRealTimers()` +- Advance fake timers forward with: `jest.advanceTimersByTime(interval)` +- Run **all timers** to completion with: `jest.runAllTimers()` +- Run **currently pending timers** to completion with: `jest.runOnlyPendingTimers()` 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. -Note: you do not need to advance timers by hand when using User Event, it's automatically. \ No newline at end of file +You can use both built-in Jest fake timers, as well as [Sinon.JS fake timers](https://sinonjs.org/releases/latest/fake-timers/). + +Note: you do not need to advance timers by hand when using User Event API, as it's automatically. diff --git a/website/docs/12.x/docs/api/misc/async.mdx b/website/docs/12.x/docs/api/misc/async.mdx index 3e251361e..5ae3cf1c8 100644 --- a/website/docs/12.x/docs/api/misc/async.mdx +++ b/website/docs/12.x/docs/api/misc/async.mdx @@ -9,8 +9,8 @@ The `findBy*` queries are used to find elements that are not instantly available ```tsx function waitFor( expectation: () => T, - { timeout: number = 1000, interval: number = 50 } -): Promise {} + options?: { timeout: number; interval: number } +): Promise; ``` 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 ```ts function waitForElementToBeRemoved( expectation: () => T, - { timeout: number = 4500, interval: number = 50 } -): Promise {} + options?: { timeout: number; interval: number } +): Promise; ``` 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.