Welcome back to this series on unit-testing Svelte. I hope youâre enjoying it so far.
In this post Iâll explore mocking, which as a topic has attracted a lot of negative attention in the JavaScript world. I want to show you the positive side of mocking and teach you how you can make effective use of test doubles.
Feedback from the first five posts
Before we get started though, Iâve got to talk about the responses Iâve received so far on Twitter. Itâs been so encouraging to see my tweet about this series retweeted and to have heard back from others about their own ways of testing.
It is so important that people who believe in testing get together and collaborate, because otherwise our voices get lost. Itâs up to us to continue to find the useful solutions for what we want to do.
Cypress variant
Hats off to Gleb Bahmutov who ported my solution from the last part to Cypress.
bahmutov / cypress-svelte-unit-test
Unit testing Svelte components in Cypress E2E test runner
I have to admit I have avoided Cypress for a while. My last project has some Cypress tests but I never really considered it for unit testing! Looking at the ported code makes me curiousâIâll come back to this in future.
Luna test runner
The author of Luna got in touch to show how simple Luna Svelte tests can be. I hadnât seen this test framework before but it has a focus on no-configuration and supports ES6. Very interesting and something I need to look into further.
On the debate between Jest, Mocha and Jasmine, and testing-library
The test techniques Iâm using in this series of posts will work in pretty much any test runner. Although which tool you use is a crucial decision youâll have to make, itâs not the point Iâm trying to make in this series. Iâm trying to show what I consider to be âgoodâ unit tests.
As for the question of testing-library, Iâm going to save this discussion for another blog post as I need to organize my thoughts still đ¤Ł
Okay, letâs get on with the main event!
Why use test doubles?
A test double is any object that stands in for another one during a test run. In terms of Svelte components, you can use test doubles to replace child components within a test suite for the parent component. For example, if you had a spec/ParentComponent.spec.js
file that tests ParentComponent
, and ParentComponent
renders a ChildComponent
, then you can use a test double to replace ChildComponent
. Replacing it means the original doesnât get instantiated, mounted or rendered: your double does instead.
Here are four reasons why you would want to do this.
- To decrease test surface area, so that any test failure in the child component doesnât break every test where the parent component uses that child.
- So that you can neatly separate tests for the parent component and for the child component. If you donât, your tests for the parent component are indirectly testing the child, which is overtesting.
- Because mounting your child component causes side effects to occur (such as network requests via
fetch
) that you donât want to happen. Stubbing outfetch
in the parent specs would be placing knowledge about the internals of the child in the parentâs test suite, which again leads to brittleness. - Because you want to verify some specifics about how the child was rendered, like what props were passed or how many times it was rendered and in what order.
If none of that makes sense, donât worry, the example will explain it well enough.
A sample child component
Imagine you have TagList.svelte
which allows a user to enter a set of space-separated tags in an input list. It uses a two-way binding to return take in tags as an array and send them back out as an array.
The source of this component is below, but donât worry about it too muchâitâs only here for reference. This post doesnât have any tests for this particular component.
<script>
export let tags = [];
const { tags: inputTags, ...inputProps } = $$props;
const tagsToArray = stringValue => (
stringValue.split(' ').map(t => t.trim()).filter(s => s !== ""));
let stringValue = inputTags.join(" ");
$: tags = tagsToArray(stringValue);
</script>
<input
type="text"
value="{stringValue}"
on:input="{({ target: { value } }) => tags = tagsToArray(value)}"
{...inputProps} />
Now we have the Post
component, which allows the user to enter a blog post. A blog post consists of some content and some tags. Here it is:
<script>
import TagList from "./TagList.svelte";
export let tags = [];
export let content = '';
</script>
<textarea bind:value={content} />
<TagList bind:tags={tags} />
For the moment we donât need to worry about savePost
; weâll come back to that later.
In our tests for Post
, weâre going to stub out TagList
. Hereâs the full first test together with imports. Weâll break it down after.
import Post from "../src/Post.svelte";
import { mount, asSvelteComponent } from "./support/svelte.js";
import
TagList, {
rewire as rewire$TagList,
restore } from "../src/TagList.svelte";
import { componentDouble } from "svelte-component-double";
import { registerDoubleMatchers } from "svelte-component-double/matchers/jasmine.js";
describe(Post.name, () => {
asSvelteComponent();
beforeEach(registerDoubleMatchers);
beforeEach(() => {
rewire$TagList(componentDouble(TagList));
});
afterEach(() => {
restore();
});
it("renders a TagList with tags prop", () => {
mount(Post, { tags: ["a", "b", "c" ] });
expect(TagList)
.toBeRenderedWithProps({ tags: [ "a", "b", "c" ] });
});
});
There's a few things to talk about here: rewire
, svelte-component-double
and the matcher plus its registration.
Rewiring default exports (like all Svelte components)
Letâs look at that rewire
import again.
import
TagList, {
rewire as rewire$TagList,
restore } from "../src/TagList.svelte";
If you remember from the previous post in this series, I used babel-plugin-rewire-exports to mock the fetch
function. This time Iâll do the same thing but for the TagList
component.
Notice that the imported function is rewire
and I rename the import to be rewire$TagList
. The rewire plugin will provide rewire
as the rewire function for the default export, and all Svelte components are exported as default exports.
Using svelte-component-double
This is a library I created for this very specific purpose.
dirv / svelte-component-double
A simple test double for Svelte 3 components
Itâs still experimental and I would love your feedback on if you find it useful.
You use it by calling componentDouble
which creates a new Svelte component based on the component you pass to it. You then need to replace the orginal component with your own. Like this:
rewire$TagList(componentDouble(TagList));
You should make sure to restore the original once youâre done by calling restore
. If youâre mocking multiple components in your test suite you should rename restore
to, for example, restore$TagList
so that itâs clear which restore
refers to which component.
Once your double is in place, you can then mount your component under test as normal.
Then you have a few matchers available to you to check that your double was in fact rendered, and that it was rendered with the right props. The matcher Iâve used here it toBeRenderedWithProps
.
The matchers
First you need to register the matchers. Since Iâm using Jasmine here Iâve imported the function registerDoubleMatchers
and called that in a beforeEach
. The package also contains Jest matchers, which are imported slightly different as they act globally once theyâre registered.
The matcher Iâve used, toBeRenderedWithProp
, checks two things:
- that the component was rendered in the global DOM container
- that the component was rendered with the right props
In addition, it checks that itâs the same component instance that matches the two conditions above.
That's important because I could have been devious and written this:
<script>
import TagList from "./TagList.svelte";
export let tags;
new TagList({ target: global.container, props: { tags } });
</script>
<TagList />
In this case there are two TagList
instances instantiated but only one that is rendered, and itâs the one without props thatâs rendered.
How it works
The component double inserts this into the DOM:
<div class="spy-TagList" id="spy-TagList-0"></div>
If you write console.log(container.outerHTML)
in your test youâll see it there. Each time you render a TagList
instance, the instance number in the id
attribute increments. In addition, the component double itself has a calls
property that records the props that were passed to it.
Testing two-way bindings
Now imagine that the Post
component makes a call to savePost
each time that tags or content change.
<script>
import TagList from "./TagList.svelte";
import { savePost } from "./api.js";
export let tags = [];
export let content = '';
$: savePost({ tags, content });
</script>
<textarea bind:value={content} />
<TagList bind:tags={tags} />
How can we test that savePost
is called with the correct values? In other words, how do we prove that TagList
was rendered with bind:tags={tags}
and not just a standard prop tags={tags}
?
The component double has a updateBoundValue
function that does exactly that.
Hereâs a test.
it("saves post when TagList updates tags", async () => {
rewire$savePost(jasmine.createSpy());
const component = mount(Post, { tags: [] });
TagList.firstInstance().updateBoundValue(
component, "tags", ["a", "b", "c" ]);
await tick();
expect(savePost).toHaveBeenCalledWith({ tags: ["a", "b", "c"], content: "" });
});
In this example, both savePost
and TagList
are rewired. The call to TagList.firstInstance().updateBoundValue
updates the binding in component
, which is the component under test.
This functionality depends on internal Svelte component state. As far as I can tell, there isnât a public way to update bindings programmatically. The updateBoundValue
could very well break in future. In fact, it did break between versions 3.15 and 3.16 of Svelte.
Why not just put the TagList
tests into Post
?
The obvious question here is why go to all this trouble? You can just allow TagList
to render its input
field and test that directly.
There are two reasons:
The
input
field is an implementation detail ofTagList
. ThePost
component cares about an array of tags, butTagList
cares about a string which it then converts to an array. Your test for saving a post would have to update theinput
field with the string form of tags, not an array. So now yourPost
tests have knowledge of howTagList
works.If you want to use
TagList
elsewhere, youâll have to repeat the same testing ofTagList
. In the case ofTagList
this isnât a dealbreaker because itâs a singleinput
field with little behaviour. But if it was a longer component, youâd need a bunch of tests specifically forTagList
.
Limitations of this approach
The component double doesnât verify that youâre passing the props that the mocked component actually exports. If you change the props of the child but forget to update anywhere itâs rendered, your tests will still pass happily.
In the next post weâll look at another approach to testing parent-child relationships which doesnât rely on mocking but is only useful in some specific scenarios, like when the both components use the context API to share information.