Chapter 3.4.3: Pseudo Async Code
A uniform test driver must support the APIs of different frameworks. The most challenging aspect of this is to reconcile async and sync APIs.
Disclaimer: You are reading an early version of the text! The final version of the book will be revised and may contain additional content.
When our code communicates with browser APIs, getting a response may take a while. For example, rendering HTML can take a few milliseconds, and loading pages altogether can take several seconds. Typically, we use promises, and async/await
in such cases. But if we write tests with Cypress, we will notice that there are no promises and no async/await
. On the contrary. If we try to use promises, Cypress will present us with an error in the console.
So is Cypress code synchronous? Not really. However, the Cypress API cleverly hides the asynchronicity from us. I can only speculate why Cypress does us this (dubious) favor. I suppose it has to do with the fact that Cypress has been around for a very long time, and the API dates from when browsers did not support Promises at all or only poorly, and async/await
was not invented yet. Hopefully, the Cypress team will modernize the API soon. But until that happens, we must accept that this circumstance makes it much more challenging to define a unified driver interface for Cypress and Vitest. But we can do it!
Vitest Driver
We will use the same trick Cypress uses: We hide the asynchrony of the Vitest implementation from the tests. Here we see a test that works according to this principle:
it(`should be possible to add a new item`, ({ driver }) => [
driver.goTo(`/`),
driver.findByLabelText(`Name`).type(`Foo bar`),
driver.findByRole(`button`, { name: `Add item` }).click(),
driver.findByText(`Foo bar`, { withinTestId: `active items` }).shouldBeVisible(),
]);
In this example, we do not see a single .then()
or await
. Nevertheless, there is an asynchronous method call hiding behind every line. The trick is that all return values (often promises) end up in an array, and we process them, promise by promise, in the it
method. Let's take a look at a concrete example:
// ...
findByText(text) {
return makeAssertions(async () => {
return screen.findByText(text);
});
},
// ...
The findByText()
method above returns the result of makeAssertions()
. And makeAssertions()
takes an asynchronous function as a parameter. This asynchronous function uses the Testing Library to find an element that contains a particular text. Next, let's look at the makeAssertions()
method:
function makeAssertions(elementResolver: ElementResolver): Assertions {
return {
shouldHaveAttribute: (attribute, value) => async () => {
let element = await elementResolver();
if (value) {
expect(element.getAttribute(attribute)).toMatch(value);
} else {
expect(element.getAttribute(attribute)).toBeTruthy();
}
},
shouldBeVisible: () => async () => {
expect(await elementResolver()).toBeTruthy();
},
// ...
};
}
makeAssertions()
returns an object with several shouldX()
methods. And this is where the magic happens: a shouldX()
method is itself synchronous but it returns an asynchronous method! Presumably, still more clarification on how the magic trick works is required. So let's look behind the curtain and take a closer look at the it()
implementation of the Vitest Driver:
Keep reading with a 7-day free trial
Subscribe to Good Tests for Vue Applications to keep reading this post and get 7 days of free access to the full post archives.