External Environment
Queries are one of the key components of storyshots, and working with them can be tricky, requiring special attention to maintain testing quality.
Ignoring query
storyshots provides methods for tracking function calls, specifically the journal. You can record any method, and this often leads to confusion. Consider the following example:
const createMockUserRepository = (): UserRepository => {
return {
getUser: async () => createUserStub(),
};
};
UserRepository contains the getUser method, which performs no side effects on the database but is nondeterministic. Since getUser belongs to the queries component, this function should not be verified.
Instead of this:
it('shows user', {
arrange: (externals, { journal }) => ({
...externals,
getUser: journal.asRecordable('getUser', externals.getUser),
}),
});
Do this:
// Do not mark the getUser method
it('shows user');
Tracking getUser is meaningless because the method does not perform side effects.
Side effects are not just results that go beyond the function’s scope; within the specification, they also include visible data to external clients:
- For the server: commands modifying the database
- For the user: functions rendering UI on the screen
These external effects are captured in the baseline by storyshots.
Interaction with getUser is verified transitively, through the component’s rendering, which uses data from the method:
const User: React.FC = () => {
const response = useQuery(userRepository.getUser);
if (response.loading) {
return <Preloader />;
}
return <UserInfo user={response.data} />;
};
This rule has one critical exception — queries like getUser, although they do not perform side effects, may implement non-trivial logic based on the arguments passed to the method.
It is recommended to record interactions with such queries in the journal.
Unstable Views
Unfortunately, it is not always possible to fully control the queries in an application. As a result, there is a risk of obtaining an unstable baseline.
An example is a third-party library component — a notification — whose final position upon appearing is not always the same, thus affecting screenshot stability.
To address this issue, use the retries function:
it('shows read notification', {
// The test will have three attempts to pass successfully
retries: (config) => 3,
});
This method is not recommended in general. It is better to either replace the problematic function or the entire library, or exclude the test scenario altogether.
Timers
UI interfaces are full of asynchronous interactions, some of which involve timers.
Consider the following example:
// Show a notification
const notification = showMessage('Message read');
// Close after 5 seconds
setTimeout(() => notification.close(), 5_000);
The above function shows a notification to the user, waits 5 seconds, and then closes it. When testing this behavior, remember that queries must not be used in stories.
Instead of this:
it('shows message to a user', {
act: (actor) => actor.screenshot('Message').wait(5_000).screenshot('Hidden'),
});
Do this:
it('shows message to a user', {
act: (actor) =>
actor
.screenshot('Message')
// Advance timers forward by 5 seconds
.exec(() => window.clock.tick(5_000))
.screenshot('Hidden'),
});
The wait function in the example waits 5 seconds before continuing test execution. This is unacceptable because test execution time is a critical metric. Therefore, in the second example, fake timers are used.
In this example, the @storyshots/web-api-mocks library is used, which performs replacement via side effects.
There is an alternative using dependency inversion to mock the API, but this method is not recommended for timers.