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.
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.
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 themulti
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 andcss
as false (I donāt write unit tests for CSS). - The
onwarn
call supressesUNRESOLVED_IMPORT
warnings. This happens because theresolve
call is set up toonly
import packages that start with the wordsvelte
. Thatās important becausesvelte
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
import
s andexport
s. - 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 theirpackage.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 theplugin-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.