Test Your Svelte Components with uvu and testing-library
When it comes to libraries & frameworks, I'm a fan of all things small & simple. So when Luke Edwards published his new test runner uvu
, I was extremely interested and had to give it a try.
In this post, I'll walk through the process of wiring up a test for UVU.
I'm learning — if you have any suggestions, tips, ideas, I'm all ears!
Setting up
Here's the component I want to test today: A simple timer.
I'd like to test if users can pause the timer without resetting it.
Code for this project is available here
Dependencies
Here's the list of all the necessary dependencies.
npm i -D uvu @testing-library/svelte jsdom jsdom-global esm pirates
name | description | url |
---|---|---|
uvu | Test runner | github |
@testing-library/svelte | Test utilities | homepage |
jsdom | DOM implementations for Node.js, @testing-library peer deps | github |
jsdom-global | Inject JSDOM api into the test environment | github |
esm | ECMAScript module loader. I'll be writing tests in the ESM format | github |
pirates | Utility to register new extension for Node.js. I use this to compile svelte code on the fly. | github |
Writing Tests
UVU has a new take on glob pattern:
Unlike other test runners, uvu intentionally does not rely on glob patterns. Parsing and matching globs both require a non-trivial amount of work. [...] This price has to be paid upfront – at startup – and then becomes completely irrelevant.
I think this approach's benefit shows in the time it takes to run the test, which we can see in a second. The test command is as follow:
uvu <dir> <pattern>
It works great to test a whole directory, but when I put test files next to their components, I need to pass an additional <pattern>
option.
src
|--Counter.svelte
|--Counter.test.js
|
...
I also need to pass in -r esm
so Node.js can understand ES6 module.
# search for files matching `/test\.js/i` in `./src`
uvu src test\.js -r esm
-r esm
-r
, or--require
is an option fornode
. Node.js will require the passed-in module (esm
in this case) at startup. It is useful for stuff like adding an extra compiling step on demand (i.e.ts-node
) or setting up the environment (i.e JSDOM).
This is what a test looks like:
// Counter.test.js
import { test } from 'uvu'
import * as assert from 'uvu/assert'
test('smoke', () => {
assert.ok(true)
})
test.run()
Nothing unusual except for the manual test.run()
.
[requiring the .run() call] Means you can easily turn on/off suites.
And, importantly, it's what actually enables programmatic testing &&
node path/to/file.js
usage (important)
$ uvu src test.js -r esm
Counter.test.js
• (1 / 1)
Total: 1
Passed: 1
Skipped: 0
Duration: 1.02ms
// success!
Now it's time to bring in testing-library
.
Testing-library
// Counter.test.js
import { test } from 'uvu'
import * as assert from 'uvu/assert'
+ import { render } from '@testing-library/svelte'
+ import Counter from './Counter.svelte'
test('smoke', () => {
+ render(Counter)
assert.ok(true)
})
test.run()
And ru—
(node:22333) UnhandledPromiseRejectionWarning: /redacted/uvu-svelte-testing-library/src/Counter.svelte:1 <script> ^ SyntaxError: Unexpected token '<'
Right, node
doesn't understand Svelte components. rollup
has my back when I develop my app, but what about the tests?
It turns out I can use a trick similar to esm
above. I can use the -r
flag to register a new extension (.svelte
) that'll compile Svelte component on the fly.
node -r import-svelte
|
require.extensions['.svelte']
|
┌────────────────┐
*.svelte --> | .svelte -> .js | --> *.js
└────────────────┘
A quick search doesn't yield any results at the time of writing, so I will have to roll my own. This gist by @jamestalmage is extremely helpful to understand how this stuff works.
In the end, I use pirates
, a library meant for this exact sort of thing. The final import-svelte.js
looks like this:
// import-svelte.js
const { addHook } = require('pirates')
const svelte = require('svelte/compiler')
function handleSvelte(code) {
const { js } = svelte.compile(code, {
dev: true,
format: 'cjs',
})
return js.code
}
addHook(handleSvelte, { exts: ['.svelte'] })
It works wonderfully — please go give pirates
a star if you haven't already!
If you're using Svelte with TS, things might get a bit more complicated — I might give it a shot later. If you'd like to give it a try, check out the source code of @mihar-22's svelte-jester which does the same thing but for Jest.
See all svelte compile options here.
And with that, we can now run our te—
$ npx uvu src test.js -r esm -r ./import-svelte.js Counter.test.js ✘ (0 / 1) FAIL "smoke" document is not defined (undefined)
Set up JSDOM
The final missing piece is the DOM. From testing-library
docs:
jsdom
is a pure JavaScript implementation of the DOM and browser APIs that runs in node. If you're not using Jest and you would like to run your tests in Node, then you must installjsdom
yourself.
I have already installed jsdom
& jsdom-global
, so it's time to use them:
uvu -r esm -r ./import-svelte.js -r jsdom-global/register src test\.js
Counter.test.js
• (1 / 1)
Total: 1
Passed: 1
Skipped: 0
Duration: 18.43ms
Lightning-fast! Now I can finally write a real test.
import { test } from 'uvu'
import * as assert from 'uvu/assert'
import { render, fireEvent } from '@testing-library/svelte'
import Counter from './Counter.svelte'
const wait = (ms) => new Promise(res => setTimeout(res, ms))
test('The counter can be paused', async () => {
const { getByText, getByDisplayValue } = render(Counter)
const $input = getByDisplayValue('20')
const $btnStart = getByText('Start')
await fireEvent.click($btnStart)
await wait(1000)
const $btnPause = getByText('Pause')
await fireEvent.click($btnPause)
assert.is($input.value, '19')
})
test.run()
Is my code garbage? Tell me! @dereknguyen10
Some thoughts
uvu
uvu
is lightning fast! But some notes:
- Error messages don't give me as much context. I learned to test Svelte components with
tap
, which give me more info. For example, when I encountered thedocument is not defined
error,tap
pointed to the trouble line of code:
$ tap --no-coverage-report --node-arg=--require=./import-svelte.js
FAIL src/components/Input.test.js
✖ document is not defined
node_modules/@testing-library/svelte/dist/pure.js
50 | options = _objectWithoutProperties(_ref, ["target"]);
51 |
> 52 | container = container || document.body;
| ---------------------------^
53 | target = target || container.appendChild(document.createElement('div'));
- When testing with snapshot,
uvu
doesn't automatically update snapshot, which one could say is a feature.
Automatic snapshot updates are missing intentionally. Instead, uvu uses direct string comparison (basically inline snapshots) so that you know & see what you're expecting. Tucking that away is not ideal & having a mechanism that auto-sweeps changes under the rug is troubling Luke's tweet
- No parallel tests for now, but it looks like it's coming.
import-svelte
Lots of important issues:
- It doesn't cache anything
- It doesn't handle preprocessor stuff, so no postcss, typescript, sass, etc.
I'll try to solve these if I encounter them, but someone smarter than me, please write a proper import-svelte
module, I beg thee!