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.

source

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 for node. 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)

Luke's tweet on the subject

$ 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 install jsdom 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 the document 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!