DEV Community

Daniel Irvine 🏳️‍🌈
Daniel Irvine 🏳️‍🌈

Posted on • Edited on

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.

Top comments (1)

Collapse
 
aakashgoplani profile image
Aakash Goplani

Do we have types defined for this package? In typescript file it gives errors:

Cannot find module 'svelte-component-double/vitest' or its corresponding type declarations.

svelte-component-double@2.0.0_jsdom@21.1.1_svelte@3.54.0/node_modules/svelte-component-double/index.js' implicitly has an 'any' type.
Try npm i --save-dev @types/svelte-component-double if it exists or add a new declaration (.d.ts) file containing `declare module 'svelte-component-double';