So, you have started to write a nice app or library in Solid.js and TypeScript - good choice, by the way - but now you want to unit test everything to make sure nothing breaks.
Jest is currently one of the best options for front end testing, but it requires some setup to play nicely with Solid.js. There are two options:
-
solid-jest
- a preset to setup jest transpile solid using babel, given a working babel configuration; won't type-check, but is faster -
ts-jest
- a module to use TypeScript with jest that needs to be coerced a bit to work with Solid.js
If you are using TypeScript, choosing solid-jest
might save you time when running the tests at the expense of missing type checks, but those can be run separately. ts-jest
will check the types for you, but will take a bit longer. Both choices are valid, so decide for yourself.
Configuration
Regardless if you use solid-jest or ts-jest, you will need a babel config that supports Solid.js - the main difference is where to put it. It looks like this:
{
"presets": [
"@babel/preset-env",
"babel-preset-solid",
// only if you use TS with solid-jest
"@babel/preset-typescript"
]
}
To be able to run it, you need to add the dev dependencies @babel/core
, @babel/preset-env
and optionally @babel/preset-typescript
depending on if you use TypeScript or not.
If you decided to use solid-jest
, then put the babel config in your .babelrc
file at the project root; otherwise it goes into the jest config section e.g. in package.json/jest as shown in the upcoming ts-jest
section.
solid-jest
For solid-jest
, the jest config that requires .babelrc
to run looks like this (without the comment):
{
"jest" : {
"preset": "solid-jest/preset/browser",
// insert setupFiles and other config
}
}
ts-jest
If you use ts-jest
instead, it looks like this (without the comments):
{
"jest": {
"preset": "ts-jest",
"globals": {
"ts-jest": {
"tsconfig": "tsconfig.json",
"babelConfig": {
"presets": [
"babel-preset-solid",
"@babel/preset-env"
]
}
}
},
// insert setupFiles and other config
// you probably want to test in browser mode:
"testEnvironment": "jsdom",
// unfortunately, solid cannot detect browser mode here,
// so we need to manually point it to the right versions:
"moduleNameMapper": {
"solid-js/web": "<rootDir>/node_modules/solid-js/web/dist/web.cjs",
"solid-js": "<rootDir>/node_modules/solid-js/dist/solid.cjs"
}
}
}
What now?
I assume you already know your way around jest. If not, let me point you at this article series about jest. So why is here more text, you ask? There are a few more considerations when testing Solid.js code:
- effects and memos only work inside a reactive root
- components output HTMLElements or functions that returns them (either a single one or an array).
That means you can use a few shortcuts instead of actual rendering everything and look at it from a DOM perspective. Let's look at the available shortcuts:
Testing custom primitives ("hooks")
A powerful way to make functionality reusable is to put it into a separate primitive function. Let's have a naive implementation of a function that returns a variable number of words of "Lorem ipsum" text:
const loremIpsumWords = 'Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'.split(/\s+/);
const createLorem = (words: Accessor<number> | number) => {
return createMemo(() => {
const output = [],
len = typeof words === 'function' ? words() : words;
while (output.length <= len) {
output.push(...loremIpsumWords);
}
return output.slice(0, len).join(' ');
});
};
If we use words
as an Accessor, it will only ever be updated inside a reactive root and the updates only available inside an effect. Luckily, jest will happily wait for async functions, so we can use a promise to collect the output that will finally be evaluated.
test(
'it updates the result when words update',
async () => {
const input = [3, 2, 5],
expectedOutput = [
'Lorem ipsum dolor',
'Lorem ipsum',
'Lorem ipsum dolor sit amet'
];
const actualOutput = await new Promise<string[]>(resolve => createRoot(dispose => {
const [words, setWords] = createSignal(input.shift() ?? 3);
const lorem = createLorem(words);
const output: string[] = [];
createEffect(() => {
// effects are batched, so the escape condition needs
// to run after the output is complete:
if (input.length === 0) {
dispose();
resolve(output);
}
output.push(lorem());
setWords(input.shift() ?? 0);
});
}));
expect(actualOutput).toEqual(expectedOutput);
}
);
This way, we don't even need to render the output and are very flexible with the test cases.
Testing directives (use:...
)
A custom directive is merely a primitive that receives a DOM reference and an Accessor with arguments, if there are any. Let's consider a directive that abstracts the Fullscreen API. It would have the following signature:
export type FullscreenDirective = (
ref: HTMLElement,
active: Accessor<boolean | FullscreenOptions>
) => void;
and is used like this:
const [fs, setFs] = createSignal(false);
return <div use:FullscreenDirective={fs}>...</div>;
Do we need to render now to test the directive? No, we don't! We can just create our HTMLElement by using document.createElement('div')
and test it like our previous primitive.
While you could argue that you want to test things from the user's perspective, the user of a directive is still a developer that uses it in the component - and you don't need to test if Solid.js actually works, that's already done for you by the maintainers.
You can have a look at an actual example at the fullscreen primitive from solid-primitives
written by yours truly.
Testing components
Finally, we get to use render!? No? Don't tell me you're testing the returned DOM elements without ever rendering them!?
Well, you certainly could do that, but not even I'm suggesting you should, because components are usually used in a DOM context and not solely in a component context, as rendering could introduce side effects. Instead, I want to point you at Solid's testing library.
So let's add solid-testing-library
to our project, together with @testing-library/jest-dom
and configure it:
// in package.json
{
// ...
"jest": {
"preset": "solid-jest/preset/browser",
"setupFilesAfterEnv": ["./src/setupTests.ts"]
}
}
// and in ./src/setupTests.ts:
import "regenerator-runtime/runtime";
import '@testing-library/jest-dom'
Our project contains the following nonsensical component that we want be tested:
import { createSignal, Component, JSX } from 'solid-js';
export const MyComponent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [clicked, setClicked] = createSignal(false);
return <div {...props} role="button" onClick={() => setClicked(true)}>
{clicked() ? 'Test this!' : 'Click me!'}
</div>;
};
Now let's write a simple test using the testing library:
import { screen, render, fireEvent } from 'solid-testing-library';
import { MyComponent } from './my-component';
test('changes text on click', async () => {
await render(() => <MyComponent />);
const component = await screen.findByRole('button', { name: 'Click me!' });
expect(component).toBeInTheDocument();
fireEvent.click(component);
expect(await screen.findByRole('button', { name: 'Test this!' })).toBeInTheDocument();
});
Let's run this:
> jest
PASS src/testing.test.tsx
✓ changes text on click (53 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.012 s
Ran all test suites.
May your tests catch all the bugs!
Top comments (7)
Thank You.
To wrap my head around
createRoot
I needed something much more basic.Leaving it here in case somebody else might find it useful.
Really useful. Thank you for the config :)
You may want to have a look at vitest, which can easily replace jest while being smaller and faster.
There's now an official starter template with typescript, vitest, solid-testing-library and jest-dom included that can serve as an example.
Gonna give it a try :)
Wow that was really informative, thank you! Will definitely use the stack for my next project
If you like me want to go beyond jest, my next post is for you: dev.to/lexlohr/testing-solidjs-cod...
And if you don't want to miss out on jest-like APIs, vitest has your back: dev.to/lexlohr/testing-your-solidj...