Mocking Svelte components

d_ir

Daniel Irvine šŸ³ļøā€šŸŒˆ

Posted on January 14, 2020

Mocking Svelte components

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.

GitHub logo 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.

  1. 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.
  2. 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.
  3. Because mounting your child component causes side effects to occur (such as network requests via fetch) that you donā€™t want to happen. Stubbing out fetch 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.
  4. 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} />
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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" ] });
  });
});
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo 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));
Enter fullscreen mode Exit fullscreen mode

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 /> 
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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: "" });
});
Enter fullscreen mode Exit fullscreen mode

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 of TagList. The Post component cares about an array of tags, but TagList cares about a string which it then converts to an array. Your test for saving a post would have to update the input field with the string form of tags, not an array. So now your Post tests have knowledge of how TagList works.

  • If you want to use TagList elsewhere, youā€™ll have to repeat the same testing of TagList. In the case of TagList this isnā€™t a dealbreaker because itā€™s a single input field with little behaviour. But if it was a longer component, youā€™d need a bunch of tests specifically for TagList.

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.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
d_ir

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

I18n with Svelte and Jest Tests
javascript I18n with Svelte and Jest Tests

July 19, 2022

Tips for writing great Svelte tests
javascript Tips for writing great Svelte tests

January 17, 2020

Mocking Svelte components
javascript Mocking Svelte components

January 14, 2020