DEV Community

Cover image for Testing a SolidJS Component Using Vitest
Matti Bar-Zeev
Matti Bar-Zeev

Posted on • Edited on

Testing a SolidJS Component Using Vitest

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
Enter fullscreen mode Exit fullscreen mode

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,
 },
});
Enter fullscreen mode Exit fullscreen mode

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"
   },
Enter fullscreen mode Exit fullscreen mode

And let’s try and run our tests…

yarn test
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
   });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 />);
   });
});
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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/],
       },
   },
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'],
   },
});
Enter fullscreen mode Exit fullscreen mode

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();
   });
});
Enter fullscreen mode Exit fullscreen mode

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();
   });
});
Enter fullscreen mode Exit fullscreen mode

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 🍻

Top comments (4)

Collapse
 
lexlohr profile image
Alex Lohr

Hi, here's Alex, the regular solid.js testing guy. Thanks for that article, because I haven't found the time to update mine, but I wanted to update our vite plugin first so vitest is supported out of the box again. Maybe you want to join my efforts?

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

Hi Alex, sounds interesting, what help are you looking for?

Collapse
 
lexlohr profile image
Alex Lohr

I haven't yet found the time to look at vite plugins, but I want to find out if it's possible to change the config dynamically.

Thread Thread
 
mbarzeev profile image
Matti Bar-Zeev

Update post can be found here: dev.to/mbarzeev/update-testing-a-s...
Cheers!