In the last part we set up our testing environment ready for tests. Now we’ll set up JSDOM, mount our component under test, and then check that it rendered the right elements into the DOM tree.
As before, all this code is in the repo.
dirv / svelte-testing-demo
A demo repository for Svelte testing techniques
Here’s a simple component, in src/StaticComponent.js
.
<script>
export let who = "human";
</script>
<main>
<button>Click me, {who}!</button>
<main>
To test this, we need to mount the component into a DOM container and then look at the rendered query output.
Before writing the test we’ll need to build up some helper functions to help us out. But here’s a sneak-peak of the first test we’ll end up writing.
it("renders a button", () => {
mount(StaticComponent);
expect(container).toMatchSelector("button");
});
Notice the custom matcher toMatchSelector
. In this post I’ll write a Jasmine matcher for that, which translates fairly simply to Jest and to other matcher systems too.
Setting up JSDOM
Before mounting a component, we’ll need to ensure that the DOM is available.
The file spec/support/svelte.js
contains the following helper function.
import { JSDOM } from "jsdom";
const setupGlobalJsdom = (url = "https://localhost") => {
const dom = new JSDOM("", { url, pretendToBeVisual: true });
global.document = dom.window.document;
global.window = { ...global.window, ...dom.window };
global.navigator = dom.window.navigator;
};
This creates a new JSDOM instance and assigns it to the document
, window
and navigator
globals. There’s a few interesting points:
- The URL can be passed in which helps if you’re testing any behavior that relies on the current page location
- The
window
object merges in the existingwindow
object if it exists, which allows you to stub out functions likewindow.fetch
before you set up the DOM. - The option
pretendToBeVisual
means that therequestAnimationFrame
API is enabled for testing, which will be useful if your app calls that function.
More information about JSDOM can be found in the JSDOM GitHub README. One point is that they recommend against setting the document as a global in the way we are here. But unfortunately if you don’t do that, Svelte won’t work as it expects to find a global document
instance.
Creating a container for each test
The spec/support/svelte.js
file also contains this function.
const createContainer = () => {
global.container = document.createElement("div");
document.body.appendChild(container);
};
That’s straightforward enough; and what you might expect. It’s not strictly necessary to append the container to the document.body
node, by the way. There are probably some use cases where it is necessary, but for many tests it won’t be.
Now there’s one more thing to do: define a function to call both setupGlobalJsdom
and createContainer
. However, it also does one more thing. Take a look.
let mountedComponents;
export const setDomDocument = url => {
setupGlobalJsdom(url);
createContainer();
mountedComponents = [];
};
This function also initializes a mountedComponents
array. This array will be used to unmount all components after a test is complete. We’ll come back to that when we define the unmount
function later.
Notice this function is also defined as an export
so we’re ready to use it in our tests.
So how do we use this in our tests? Like this, in the file spec/StaticComponent.spec.js
. By the way, this isn’t the end result—we’re going to improve on this later in this post.
import { setDomDocument } from "./support/svelte.js";
describe(StaticComponent.name, () => {
beforeEach(() => setDomDocument());
});
In case you are thinking that beforeAll
will do, I’d recommend against that. JSDOM is quick to setup and it’s always better to start each test with a clean slate.
Mounting components
Time to define mount
, which exists in the same file, spec/support/svelte.js
.
It comes in two parts: a function setBindingCallbacks
, and the mount
function itself.
import { bind, binding_callbacks } from "svelte/internal";
const setBindingCallbacks = (bindings, component) =>
Object.keys(bindings).forEach(binding => {
binding_callbacks.push(() => {
bind(mounted, binding, value => {
bindings[binding] = value
});
});
});
export const mount = (component, props = {}, { bindings = {} } = {}) => {
const mounted = new component({
target: global.container,
props
});
setBindingCallbacks(bindings, mounted);
mountedComponents = [ mounted, ...mountedComponents ];
return mounted;
};
The setBindingCallbacks
is necessary for testing Svelte component bindings. The code here is plumbing that you don’t need to worry about.
However, since it relies on svelte/internal
it is subject to change and this API could break in future. I’ll come back to this in a future part; it turns out that testing bindings (both one-way and two-way) is not straightforward.
Interestingly enough, the component is always mounted at the container root. The test gets no choice about that. Each test you write will only have the option of mounting one component under test at any one time. This is standard for unit testing.
Unmounting
Now let’s look at how we can unmount components. We should do this after each test using an afterEach
call.
When I was unit testing React, unmounting components wasn’t always necessary. But with Svelte, I find it is pretty essential. Tests will often break subsequent tests if this isn’t done. I don’t know enough of the Svelte internals to know why that is.
By the way, if you were writing onDestroy
handlers you’d be using this function (which also appears in spec/support/svelte.js
) as part of the act phase of your test, not the arrange phase.
export const unmountAll = () => {
mountedComponents.forEach(component => {
component.$destroy()
});
mountedComponents = [];
};
Putting it together: our first test
Here’s what a first test looks like.
import { mount, setDomDocument, unmountAll } from "./support/svelte.js";
import StaticComponent from "../src/StaticComponent.svelte";
describe(StaticComponent.name, () => {
beforeEach(() => setDomDocument());
afterEach(unmountAll);
it("renders a button", () => {
mount(StaticComponent);
expect(container.querySelector("button")).not.toBe(null);
});
});
There are two things I want to improve on this:
- Creating a more descriptive matcher,
toMatchSelector
, which beats anot.toBe(null)
any day. - Pull the
beforeEach
andafterEach
into their own function as a helper.
Quick side note: Writing beforeEach(setDomDocument)
won’t work as Jasmine actually passes an argument to beforeEach
blocks, which our helper would pick up as the url
parameter.
Defining a toMatchSelector
matcher
The reason this is important is so that if it fails, your test exception tells you as much useful information as possible.
Let’s take the example above to see what I mean. Taking this expectation:
expect(container.querySelector("button")).not.toBe(null);
When this expectation fails, the output is this:
Expected null not to be null. Tip: To check for deep equality, use .toEqual() instead of .toBe().
This is totally useless. Expected null not to be null
. Great!
How about this instead?
Expected container to match CSS selector "button" but it did not.
Much more useful. So let’s write a custom matcher to do that.
This is a Jasmine custom matcher but a Jest custom matcher looks very similar, except it has a slightly nicer API and it’s easy to add pretty colors to the output.
This matcher also lives in spec/support/svelte.js
.
const toMatchSelector = (util, customEqualityTesters) => ({
compare: (container, selector) => {
if (container.querySelector(selector) === null) {
return {
pass: false,
message: `Expected container to match CSS selector "${selector}" but it did not.`
}
} else {
return {
pass: true,
message: `Expected container not to match CSS selector "${selector}" but it did.`
}
}
}
});
You’ll notice I haven’t marked this as export
, but we need some way for our test to register this matcher with Jasmine. I’m going to skip ahead and tie this in to the second of our refactorings above, of pulling the beforeEach
and afterEach
into their own helper.
Defining asSvelteComponent
This is a real beauty:
export const asSvelteComponent = () => {
beforeEach(() => setDomDocument());
beforeAll(() => {
jasmine.addMatchers({ toMatchSelector });
});
afterEach(unmountAll);
};
Isn’t that lovely?
Now let’s rewrite our test to use this new set up:
import { mount, asSvelteComponent } from "./support/svelte.js";
import StaticComponent from "../src/StaticComponent.svelte";
describe(StaticComponent.name, () => {
asSvelteComponent();
it("renders a button", () => {
mount(StaticComponent);
expect(container).toMatchSelector("button");
});
});
Elegant, concise, and it works. Yum.
Testing props and adding the element
helper
To finish off this part, here’s two more tests, together with an element
helper function.
const element = selector => container.querySelector(selector);
it("renders a default name of human if no 'who' prop passed", () => {
mount(StaticComponent);
expect(element("button").textContent).toEqual("Click me, human!");
});
it("renders the passed 'who' prop in the button caption", () => {
mount(StaticComponent, { who: "Daniel" });
expect(element("button").textContent).toEqual("Click me, Daniel!");
});
I like the element
helper because it allows our expectations to read like “proper” English.
You can also define elements
like this:
const elements = selector =>
Array.from(container.querySelectorAll(selector));
That’s it for this part. We’ve now built up a good selection of helpers that allows us to write clear, concise tests. In the next section we’ll look at testing onMount
callbacks.
Top comments (1)
Hello, Thanks for the guides! I want to ask. I am new in Frontend testing, If we have 2 buttons in a component, how to select / test each specific button?