Matti Bar-Zeev
Posted on July 8, 2022
Gear up cause in this week’s post we’re going to introduce Vitest to a SolidJS project and test a single component.
Yes, I know that there is a project template for it, but in order to understand better what it takes to include Vitest unit testing in your existing SolidJS project I will start from the bare minimum and add the required dependencies and configuration so that I will be able to run my first unit test with it.
Be warned - this is not a journey for the faint hearted, so I’ve learned along the way… ;)
Let’s go
I think the efficient way to go about this is to first introduce Vitest to the project, so I start with installing it
yarn add -D vitest
If you’re like me and started from the “simple” Solid template you should have a vite configuration file on your project root, vite.config.js
, with the following content:
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
export default defineConfig({
plugins: [solidPlugin()],
build: {
target: 'esnext',
polyfillDynamicImport: false,
},
});
Vitest is supposed to read the configuration file OOTB and be able to work with it.
Next I will add a “test” script to my package.json
file like so:
"scripts": {
...
"test": "vitest"
},
And let’s try and run our tests…
yarn test
Sure enough we’re getting an error message that there are no tests found, which is actually great since it means that Vitest is set and ready. Let’s add our first test.
The Timer Component
I have a Timer component with a very simple responsibility - it should display the time in a “mm:ss” format given a certain number of seconds, for example, if it gets 120 seconds it should display 02:00.
This component is “reacting” to the application store’s timerSeconds
and when that changes it knows to render the time string again. Here’s the code for the component:
import styles from './index.module.css';
import {gameState} from '../../stores/game-store';
const Timer = () => {
return <div class={styles.Timer}>{getDisplayTimeBySeconds(gameState.timerSeconds)}</div>;
};
const getDisplayTimeBySeconds = (seconds) => {
const min = Math.floor(seconds / 60);
const sec = seconds % 60;
return `${getDisplayableTime(min)}:${getDisplayableTime(sec)}`;
};
function getDisplayableTime(timeValue) {
return timeValue < 10 ? `0${timeValue}` : `${timeValue}`;
}
export default Timer;
I will now create the test for it, in the same directory, and initialize it with a dummy assertion just to make sure Vitest runs it well:
import {describe, it, expect} from 'vitest';
describe('Timer component', () => {
it('should assert some dummy assertion', () => {
expect(1).toBeTruthy();
});
});
Yep, it runs and passes alright.
Ok, so what we would like to do now is render the component and start testing it. For that we need the equivalent of the react-testing-library for solid, which is (sit down for this) Solid-Testing-Library.
Adding it to my dev dependencies and moving on -
yarn add -D solid-testing-library
Now let’s try to render our component and see what hell breaks loose…
import {describe, it, expect} from 'vitest';
import {render} from 'solid-testing-library';
import Timer from '.';
describe('Timer component', () => {
it('should assert some dummy assertion', () => {
render(<Timer />);
});
});
Running the test and I get this error:
FAIL src/components/Timer/index.test.jsx [ src/components/Timer/index.test.jsx ]
SyntaxError: The requested module 'solid-js/web' does not provide an export named 'hydrate'
Was I too naive to think this will run smoothly? But of course.
It appears that this is a known issue and a suggested workaround can be found here. I will try that suggestion. My vite configuration looks like this now:
export default defineConfig({
plugins: [solidPlugin()],
build: {
target: 'esnext',
polyfillDynamicImport: false,
},
test: {
deps: {
inline: [/solid-testing-library/],
},
},
});
And now when I run the test it fails on a different thing:
FAIL src/components/Timer/index.test.jsx [ src/components/Timer/index.test.jsx ]
TypeError: template is not a function
Seriously?
Ok, at this point I’m going to look for the configuration the vitest “template” comes with and understand what’s going on there. It seems that the integration with Vitest relies heavily on Jest ecosystem libs, which kinda sucks, but let’s roll with it.
I’m installing these dependencies first:
- @testing-library/jest-dom
- jsdom
After that I’m creating a setup file, named setupVitest.js
which basically imports the @testing-library/jest-dom
.
Next I’m copying the test configuration from the template but with a slight modification according to the instructions from this GitHub thread.
The “resolve conditions” in the configuration is meant to instruct Vite to resolve exported modules in a certain way. In the configuration below it instructs it to be resolved as “browser” exports for the “development” env, which I can only assume means that it uses browser supported module exporting (if you know any different, please share with us in the comments)
And so our configuration looks like this:
import {defineConfig} from 'vite';
import solidPlugin from 'vite-plugin-solid';
export default defineConfig({
plugins: [solidPlugin()],
build: {
target: 'esnext',
polyfillDynamicImport: false,
},
test: {
globals: true,
environment: 'jsdom',
transformMode: {
web: [/\.jsx?$/],
},
setupFiles: './setupVitest.js',
// solid needs to be inline to work around
// a resolution issue in vitest
// And solid-testing-library needs to be here so that the 'hydrate'
// method will be provided
deps: {
inline: [/solid-js/, /solid-testing-library/],
},
},
resolve: {
conditions: ['development', 'browser'],
},
});
let’s run the test now…
Yes! It passes, but wait… we haven’t asserted anything meaningful yet. Let’s check that the timer displays “00:00” when it renders with no timerSeconds provided:
import {describe, it, expect} from 'vitest';
import {render, screen} from 'solid-testing-library';
import Timer from '.';
describe('Timer component', () => {
it('should render the timer', () => {
render(() => <Timer />);
const timerElm = screen.getByText('00:00');
expect(timerElm).toBeInTheDocument();
});
});
Yep, passes :)
(I obviously checked it failed to make sure my test does not “lie” to me)
Next let’s assert that once it gets 123 seconds it displays “02:03”. For this I’m gonna mock the store as if it was a simple object with a getter function on it (which, let’s face it, is pretty much what it is). What I like about this is that I don’t need to wrap my components in weird context providers just to access the store. That makes this a lot simpler in my eyes.
import {describe, it, expect} from 'vitest';
import {render, screen} from 'solid-testing-library';
import Timer from '.';
let mockTimerSeconds = 0;
vi.mock('../../stores/game-store', () => ({
gameState: {
get timerSeconds() {
return mockTimerSeconds;
},
},
}));
describe('Timer component', () => {
it('should render the timer', () => {
render(() => <Timer />);
const timerElm = screen.getByText('00:00');
expect(timerElm).toBeInTheDocument();
});
it('should render the timer according to the store timerSeconds', () => {
mockTimerSeconds = 123;
render(() => <Timer />);
const timerElm = screen.getByText('02:03');
expect(timerElm).toBeInTheDocument();
});
});
Cool :)
Wrapping up
It was one hell of a struggle to get this configuration going and to be honest there is still work to be done to have Vitest a first-class-citizen in a Vite powered project. It also still bothers me that there are many Jest dependencies for a Vitest runner to do its work.
Having said that - it’s Vitest, my friends, and it’s blazing fast and that’s not something to take lightly.
Bottom line, a SolidJS project with Vitest test runner is up and running. Test away!
UPDATE:
Following a comment bt @lexlohr I've updated the configuration and you can find the rest of the details in this update post.
If you have any suggestions on how this can be done better, and I’m sure you do, please be sure to leave them in the comments below so that we can all learn from it and evolve :)
Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻
Posted on July 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.