Testing
Edit this pageTesting your Solid applications is important to inspiring confidence in your codebase through preventing regressions.
Getting started
Testing packages explanations
vitest
- testing framework that includes runner, assertion engine, and mocking facilitiesjsdom
- a virtual DOM used to simulate a headless browser environment running in node@solidjs/testing-library
- a library to simplify testing components, directives, and primitives, with automatic cleanup@testing-library/user-event
- used to simulate user events that are closer to reality@testing-library/jest-dom
- augments expect with helpful matchers
Adding testing packages
The recommended testing testing framework for Solid applications is vitest.
To get started with vitest, install the following development dependencies:
Testing configuration
In your package.json
add a test
script calling vitest
:
It is not necessary to add @testing-library/jest-dom
to the testing options in vite.config
, since vite-plugin-solid
automatically detects and loads it if present.
TypeScript configuration
If using TypeScript, add @testing-library/jest-dom
to tsconfig.json#compilerOptions.types
:
SolidStart configuration
When using SolidStart, create a vitest.config.ts
file:
Writing tests
Components testing
Testing components involves three main things:
- Rendering the component
- Interacting with the component
- Validating assertions
To write tests for your components, create a [name].test.tsx
file.
The purpose of this file is to describe the intended behavior from a user's perspective in the form of unit tests:
In the test.jsx
file, the render
call from @solidjs/testing-library
is used to render the component and supply the props and context.
To mimic a user interaction, @testing-library/user-event
is used.
The expect
function provided by vitest
is extended with a .ToHaveTextContent("content")
matcher from @testing-library/jest-dom
to supply what the expected behavior is for this component.
To run this test, use the following command:
If running the command is successful, you will get the following result showing whether the tests have passed or failed:
Rendering the component
The render
function from @solidjs/testing-library
creates the testing environment within the test.tsx
file.
It sets up the container, rendering the component within it, and automatically registers it for clean-up after a successful test.
Additionally, it manages wrapping the component in contexts as well as setting up a router.
Using the right queries
Queries are helpers used to find elements within a page.
The prefixes (get
, query
, and find
) and the middle portion (By
and AllBy
) depend on if the query should wait for an element to appear (or not), whether it should throw an error if the element cannot be found, and how it should handle multiple matches:
- getBy: synchronous, throws if not found or more than 1 matches
- getAllBy: synchronous, throws if not found, returns array of matches
- queryBy: synchronous, null if not found, error if more than 1 matches
- queryAllBy: synchronous, returns array of zero or more matches
- findBy: asynchronous, rejected if not found within 1000ms or more than 1 matches, resolves wth element if found
- findAllBy: asynchronous, rejected if not found within 1000ms, resolves with array of one or more element(s)
By default, queries should start with get...
.
If there are multiple elements matching the same query, getAllBy...
should be used, otherwise use getBy...
.
There are two exceptions when you should not start with get...
:
- If the
location
option is used or the component is based on resources, the router will be lazy-loaded; in this case, the first query after rendering needs to befind...
- When testing something that is not rendered, you will need to find something that will be rendered at the same time; after that, use
queryAllBy...
to test if the result is an empty array ([]
).
The query's suffix (Role, LabelText, ...) depends on the characteristics of the element you want to select. If possible, try to select for accessible attributes (roughly in the following order):
- Role: WAI ARIA landmark roles which are automatically set by semantic elements like
<button>
or otherwise userole
attribute - LabelText: elements that are described by a label wrapping the element, or by an
aria-label
attribute, or is linked withfor
- oraria-labelledby
attribute - PlaceholderText: input elements with a
placeholder
attribute - Text: searches text within all text nodes in the element, even if split over multiple nodes
- DisplayValue: form elements showing the given value (e.g. select elements)
- AltText: images with alt text
- Title: HTML elements with the
title
attribute or SVGs with the<title>
tag containing the given text - TestId: queries by the
data-testid
attribute; a different data attribute can be setup viaconfigure({testIdAttribute: 'data-my-test-attribute'})
; TestId-queries are not accessible, so use them only as a last resort.
For more information, check the testing-library documentation.
Testing through Portal
Solid allows components to break through the DOM tree structure using <Portal>
. This mechanism will still work in testing, so the content of the portals will break out of the testing container. In order to test this content, make sure to use the screen
export to query the contents:
Testing in context
If a component relies on some context, to wrap it use the wrapper
option:
Wrappers can be re-used if they are created externally. For wrappers with different values, a higher-order component creating the required wrappers can make the tests more concise:
If using multiple providers, solid-primitives has <MultiProvider>
to avoid nesting multiple levels of providers
Testing routes
For convenience, the render
function supports the location
option that wraps the rendered component in a router pointing at the given location.
Since the <Router>
component is lazily loaded, the first query after rendering needs to be asynchronous, i.e. findBy...
:
Interacting with components
Many components are not static, rather they change based on user interactions.
To test these changes, these interactions need to be simulated.
To simulate user interactions, @testing-library/user-event
library can be used.
It takes care of the usual order of events as they would occur in actual user interactions.
For example, this means that a click
event from the user would be accompanied by mousemove
, hover
, keydown
, focus
, keyup
, and keypress
.
The most convenient events to test are typically click
, keyboard
and pointer
(to simulate touch events).
To dive deeper into these events, you can learn about them in the user-event
documentation.
Using timers
If you require a fake timer and want to use vi.useFakeTimers()
in your tests, it must set it up with an advanceTimers
option:
Validating assertions
vitest
comes with the expect
function to facilitate assertions that works like:
The command supports assertions like toBe
(reference comparison) and toEqual
(value comparison) out of the box.
For testing inside the DOM, the package @testing-library/jest-dom
augments it with some helpful additional assertions:
.toBeInTheDocument()
- checks if the element actually exists in the DOM.toBeVisible()
- checks if there is no reason the element should be hidden.toHaveTextContent(content)
- checks if the text content matches.toHaveFocus()
- checks if this is the currently focused element.toHaveAccessibleDescription(description)
- checks accessible description- and a lot more.
Directive testing
Directives are reusable behaviors for elements.
They receive the HTML element they are bound to as their first and an accessor of the directive prop as their second argument.
To make testing them more concise, @solidjs/testing-library
has a renderDirective
function:
In ...renderResults
, the container will contain the targetElement
, which defaults to a <div>
.
This, along with the ability to modify the arg
signal, are helpful when testing directives.
If, for example, you have a directive that handles the Fullscreen API, you can test it like this:
Primitive testing
When the reference to an element is not needed, parts of state and logic can be put into reusable hooks or primitives.
Since these do not require elements, there is no need for render
to test them since it would require a component that has no other use.
To avoid this, there is a renderHook
utility that simulates a component without actually rendering anything.
A primitive that manages the state of a counter could be tested like this:
Testing effects
Since effects may happen asynchronously, it can be difficult to test them.
@solidjs/testing-library
comes with a testEffect
function that takes another function that receives a done
function to be called once tests are over and returns a promise.
Once done
is called, the returned promise is resolved.
Any errors that would hit the next boundary are used to reject the returned promise.
An example test using testEffect
may look like this:
Integration/E2E testing
Some issues can only be found once the code is running in the environment it is supposed to run in. Since integration and end-to-end tests are agnostic to frameworks, all proven approaches will work equally for Solid.