Mocking Svelte components
Daniel Irvine š³ļøāš
Posted on January 14, 2020
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.
Posted on January 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.