loading...

Setting up a Svelte test environment

d_ir profile image Daniel Irvine 🏳️‍🌈 ・9 min read

Writing unit tests for Svelte (8 Part Series)

1) Writing unit tests for Svelte (Introduction) 2) Setting up a Svelte test environment 3 ... 6 3) Mounting components and asserting on the DOM 4) Testing the onMount callback 5) Testing Svelte stores and mocking dependencies 6) Mocking Svelte components 7) Testing Svelte context with component hierarchies 8) Tips for writing great Svelte tests

In this part, we’ll look at all the NPM packages and config that’s need to get a basic test environment running. By the end of it you’ll know enough to run npm test to run all specs, and npm test <spec file> to run a single spec file in isolation.

Remember that you can look at all this code in the demo repo.

GitHub logo dirv / svelte-testing-demo

A demo repository for Svelte testing techniques

An overview of the process

A Svelte unit testing set up makes use of the following:

  • Node, because we’ll run our tests on the command line, outside of a browser.
  • Jasmine, which executes our test scripts. (If you find this a poor choice for yourself then feel free to replace it with Mocha, Jest, or anything else.)
  • JSDOM, which provides a DOM for Svelte to operate against in the Node environment.
  • Rollup, for bundling all our test files into a single CommonJS file that Node can run. This is where Svelte testing differs from React testing, and we’ll look at this in detail later.
  • Scripty, which makes our package.json clearer when we come to define our test build & execute script.

It’s worth repeating how this differs from something like React:

Calling npm test on the command line will cause test files to be bundled with Rollup into a single file, transpiled to CommonJS, and then that single file is passed to the test runner.

For React unit testing, it's more normal to avoid any bundler: the test runner is passed each specific spec file. Instead, Babel transpile each file as it is loaded by the Node module loader.

More on this later.

Required packages

Here’s a list of required packages, together with an explanation of why that package is necessary. You can install all of these as dev dependencies using the npm install --save-dev command, or you can copy them straight out of the demo repo’s package.json.

Package name Why?
svelte Because it’s the framework we’re testing.
rollup This is the standard Svelte bundler and it’s critical to what we’re doing. Webpack is not necessary!
jasmine The test runner we’ll be using. You can safely replace this with mocha, jest 😜
scripty This allows us to easily specify complicated npm script commands in their own files rather than jammed into the package.json file. We’ll need this for our npm test command.
source-map-support This helps our test runner convert exception stack trace locations from the build output back to their source files.
jsdom Gives us a DOM API for use in the Node environment. We won’t use this until the next part in the series.
serve This is a web server that serves our build output for “manual” testing. We will use in this part but not any of the subsequent parts.

Then there are some rollup plugins. Only the first three are really necessary for following this guide, but the rest are essential for any real-world Svelte development.

Package name Why?
rollup-plugin-svelte Compiles Svelte components into ES6 modules.
@rollup/plugin-multi-entry Allows Rollup to roll up multiple source files into out output file. Usually it takes one input file plus its dependencies and converts that into one output file, but for our tests we’ll feed it all of our spec files at once.
@rollup/plugin-node-resolve Resolve packages in node_modules.
@rollup/plugin-commonjs Allow Rollup to pull in CommonJS files, which is almost every NPM package out there. It’s generally necessary if you use any non-Svelte package.
rollup-plugin-livereload Improve your “manual” test experience by shortening the feedback loop between code changes and visual verification of the change. We won’t use this, but we will configure it.
rollup-plugin-terser Minifies code. Useful when you’re creating production builds. Again, we won’t use this, but we will configure it.

Note: I’m purposefully leaving out mock setup as that’s coming in a later part of the series, but to save you the suspense, I use babel-plugin-rewire-exports together with my own package svelte-component-double.

Rollup configuration

Our setup uses the standard rollup.config.js. Nothing changes there.

But what about our tests? How do they get built? Well, for that we use a separate configuration file. It uses a different set of plugin and imports (it doesn’t need serve configuration for example, or a startup script, or livereload or terser support).

Here is rollup.test.config.js:

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import multi from "@rollup/plugin-multi-entry";
import svelte from "rollup-plugin-svelte";

export default {
  input: "spec/**/*.spec.js",
  output: {
    sourcemap: true,
    format: "cjs",
    name: "tests",
    file: "build/bundle-tests.js"
  },
  plugins: [
    multi(),
    svelte({ css: false, dev: true }),
    resolve({
      only: [/^svelte-/]
    }),
    commonjs()
  ],
  onwarn (warning, warn) {
    if (warning.code === "UNRESOLVED_IMPORT") return;
    warn(warning);
  }
};

A few things to note about this:

  • The output format is cjs, because we’ll be running in Node, not the browser.
  • The input format is a glob (spec/**/*.spec.js) which will match all our test files as long as they match that glob. This isn’t standard Rollup behavior. It’s enabled with the multi import.
  • The output it written to ./build rather than ./public/build. These files will never be loaded in the browser.
  • The Svelte compiler gets passed a dev value of true and css as false (I don’t write unit tests for CSS).
  • The onwarn call supresses UNRESOLVED_IMPORT warnings. This happens because the resolve call is set up to only import packages that start with the word svelte. That’s important because svelte packages are the ones that tend to be ES6 modules. Other NPM modules are CommonJS, so it’s fine to let Node load them itself, rather than letting Rollup bundle them.

It’s worth discussing that last point a little bit more, as I’ve struggled with it. I’m no expert on JavaScript modules, and this whole experience has left me scratching my head on my occasions. Trying to learn about modules format, like CommonsJS and ES6, is not simple. The state of play is constantly changing, particular as Node’s support for ES6 modules is almost out the door. More on that later.

Here’s what I’ve learned:

  • Rollup is good at bundling ES6 modules together. That’s what it’s designed to do. It stitches together ES6 imports and exports.
  • As a niceity, Rollup also transpiles your bundle to CommonJS once it’s done, which is what Node understands by default.
  • Node by default treats all NPM packages as CommonJS, unless they have "type": "module" defined in their package.json, in which case they are treated as ES6 modules. This is very new to Node and most packages don’t yet follow this practice, even if they do in fact contain ES6 module code.
  • Svelte NPM packages are ES6 modules, but most won’t have had a chance yet to add this new type field.
  • Rollup assumes that every file it is given as input is an ES6 module, so it happily bundles Svelte NPM packages.
  • The @rollup/plugin-commonjs plugin generally does a good job of converting CommonJS modules to ES6 for bundling (which it will later transpile convert back to CommonJS 😆) but it trips up on some packages (for example, sinon) for reasons that are beyond my level of understanding. I believe that some of the plugin-commonjs options could be used to solve this, but I chose another route...

Instead, I set my config up in the way you see above. Rollup only gets to bundle packages that are prefixed with svelte-. Everything else gets passed to NPM, and we suppress warnings about unresolved exports.

To this point this has worked for me but I’m sure this approach won’t work forever—if you’ve any opinions please do reach out in the comments.

Let’s move on for now...

The test script

The scripty package allows us to extract non-trivial scripts out of package.json.

Here’s scripts/test (Scripty pull files from the scripts directory).

#!/usr/bin/env sh

if [ -z $1 ]; then
  npx rollup -c rollup.test.config.js
else
  npx rollup -c rollup.test.config.js -i $1
fi

if [ $? == 0 ]; then
  npx jasmine
fi

This script does two things: first, it bundles the tests using npx rollup, and second, it runs the test file using npx jasmine, assuming that the first step was successful.

You can specify an argument if you wish, in which case the -i option gets passed to Rollup and it uses only the file provided as input.

To make this script work, package.json needs to have its test script updated as follows:

"scripts": {
  "test": "scripty"
}

Now your tests can be run with either of these two commands:

npm test                           # To run all test files
npm test spec/MyComponent.spec.js  # To run just a single test file

Jasmine configuration

The final part is configuring Jasmine, which happens in the file spec/support/jasmine.json:

{
  "spec_dir": ".",
  "spec_files": [
    "build/bundle-tests.js"
  ],
  "helpers": [
    "node_modules/source-map-support/register.js"
  ],
  "random": false
}

There are two important things here:

  • The spec file is always given as build/bundle-tests.js. This never changes. It’s up to Rollup to change the contents of this file depending on whether you’re testing all files or just a single file.
  • We enabled source-map-support by registering it here. This ensures stack traces are converted from the bundled file to the original source files.

Mocha setup

If you’re using Mocha, you’d put this in your package.json:

  "mocha": {
    "spec": "build/bundle-tests.js"
  }

And you’d change the final line of scripts/test to read as follows:

npx mocha -- --require source-map-support/register

Testing it out

Time to try it out. The repository has a Svelte component within the file src/HelloComponent.svelte:

<p>Hello, world!</p>

Yes—Svelte components can be plain HTML.

We don’t yet have any means to mount this component. But we can at least check that it is indeed a Svelte component.

Here’s spec/HelloComponent.spec.js that does just that.

import HelloComponent from "../src/HelloComponent.svelte";

describe(HelloComponent.name, () => {
  it("can be instantiated", () => {
    new HelloComponent();
  });
});

Try it out with a call to npm test (or npm test spec/HelloComponent.spec.js):

created build/bundle-tests.js in 376ms
Started
F

Failures:
1) HelloComponent can be instantiated
  Message:
    Error: 'target' is a required option
  Stack:
    Error: 'target' is a required option
        at new SvelteComponentDev (/Users/daniel/svelte-testing-demo/node_modules/svelte/internal/index.mjs:1504:19)
        at new HelloComponent (/Users/daniel/svelte-testing-demo/build/bundle-tests.js:282:5)
        at UserContext.<anonymous> (/Users/daniel/svelte-testing-demo/spec/HelloComponent.spec.js:5:5)
        at <Jasmine>
        at processImmediate (internal/timers.js:439:21)

This error looks about right to me. Svelte needs a target option when instantiating a root component. For that we’ll need a DOM component. We’ll use JSDOM to create that in the next part of this series.

Is Rollup really necessary?

To wrap up this part, I thought I’d explain a little more about Rollup. I had never encountered this before, having led a relatively sheltered React + Webpack existence up until this point.

I’ll admit, using Rollup in front of my tests like this felt like the option of last resort. It was so different from what I’d done before, with having Babel transpile my ES6 source files individually when Node required them. Babel did that by hooking into the require function, and all was fine.

I tried to make this work without Rollup:

  • I tried to hook into require myself and call the Svelte compiler directly. That works until your Svelte components reference Svelte component in other packages. This won’t work because Svelte NPM packages are ES6 by defaultt, and Node can’t handle these. Only Rollup knows how to manage these packages.
  • I even wrote an experimental ES6 test runner called concise-test so that I could load ES6-only files. But this doesn’t work because the Node ES6 module loader hooks API is still too immature and I couldn’t get it to compile Svelte files correctly.
  • I thought about using Babel to compile Svelte, or using Webpack to compile and bundle, but both of these seemed going extremely off-piste, even for me.

I finally gave in to Rollup because it was only with a combination of Rollup and Babel that I was able to get mocking of components working. We'll look at how that works in the last part of the series.

Summary

In this part we look at the necessary packages and configuration for running tests. In the next part, we'll begin to write tests by mounting Svelte components into JSDOM provided containers.

Writing unit tests for Svelte (8 Part Series)

1) Writing unit tests for Svelte (Introduction) 2) Setting up a Svelte test environment 3 ... 6 3) Mounting components and asserting on the DOM 4) Testing the onMount callback 5) Testing Svelte stores and mocking dependencies 6) Mocking Svelte components 7) Testing Svelte context with component hierarchies 8) Tips for writing great Svelte tests

Posted on by:

d_ir profile

Daniel Irvine 🏳️‍🌈

@d_ir

I’m the author of Mastering React Test-Driven Development, available now from Packt. I run the Queer Code London meetup.

Discussion

markdown guide
 

I get this error when running "npm test".

Executing "e:<folder><app>\scripts\test":

npx rollup -c rollup.test.config.js -i

npx jasmine

events.js:174
throw er; // Unhandled 'error' event
^

Error: spawn e:<folder><app>\scripts\test ENOENT
at Process.ChildProcess._handle.onexit (internal/child_process.js:240:19)
at onErrorNT (internal/child_process.js:415:16)
at process._tickCallback (internal/process/next_tick.js:63:19)
Emitted 'error' event at:
at Process.ChildProcess._handle.onexit (internal/child_process.js:246:12)
at onErrorNT (internal/child_process.js:415:16)
at process._tickCallback (internal/process/next_tick.js:63:19)
npm ERR! Test failed. See above for more details.

I can run both the commands in the script file without scripty.
I don't have rollup running my production builds (I use webpack), so I don't really need the if statement in the script.

Any suggestions?

 

Am I right in thinking you’re running on Windows? I took a look through the scripty Windows instructions but I’m not sure any of it applies to your situation, other than ensuring the file is in fact executable.

What happens if you run scripts/test directly from the command line?

Other than that I’m not sure what to suggest. If you can get a minimal reproduction onto GitHub/GitLab/etc I can take a look.

 

Hello there,
Thanks for this great tutorial.
I have a problem, I get this error:

@ECHO OFF

if [ -z $1 ]; then
npx rollup -c rollup.test.config.js
else
npx rollup -c rollup.test.config.js -i $1
fi

if [ $? == 0 ]; then
npx jasmine
fi

Active code page: 1252
-z was unexpected at this time.
npm ERR! Test failed. See above for more details.

Any ideas?
What -z means by the way? Is it batch script?
I am using windows 7