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.
Top comments (10)
I get this error when running "npm test".
Executing "e:<folder><app>\scripts\test":
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.
I have this exact same issue. I cloned your repo to test if I did anything wrong, but I have the same issue with your code :/
I wonder if package updates have broken something. I’ll find some time later today to take a look at this again.
Thank you so much!
Are there any updates on this?
In the repo,
input: "spec/**/*.spec.js",
has been replaced with just the name of the single test. If I put the regex back I get the error messageCannot use import statement outside a module
when running the tests. There's no stack trace, so I'm not sure where this happens. Does anyone know a solution to this?Hey Daniel,
Thanks for the fabulous post! I'm loving Svelte and this has been a super helpful and interesting post.
I have two things that I had to change to get the code (as it is up to this point in the series) running with an empty test component:
Any insight on either of these items would be appreciated! Looking forward to the rest of the series.
Thanks!
Hello there,
Thanks for this great tutorial.
I have a problem, I get this error:
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
Hi Daniel!
Thank you a lot for this intro!
We built up a stack with this technologies:
Jest
Svelte 3
@testing-library/svelte
Typescript
Bootstrap 5
(rollup for bundling, not for tests)
node 16
In case anyone gets errors in their setup, here is a good thread to check: github.com/mihar-22/svelte-jester/...