Setting up a Svelte test environment

d_ir

Daniel Irvine šŸ³ļøā€šŸŒˆ

Posted on January 12, 2020

Setting up a Svelte test environment

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);
  }
};

Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

npx mocha -- --require source-map-support/register
Enter fullscreen mode Exit fullscreen mode

Testing it out

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

<p>Hello, world!</p>
Enter fullscreen mode Exit fullscreen mode

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

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

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.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
d_ir

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related