Testing the onMount callback

d_ir

Daniel Irvine 🏳️‍🌈

Posted on January 12, 2020

Testing the onMount callback

Welcome back to the Writing unit tests for Svelte series! Thanks for sticking with me! ❤️

In this part we’ll test an asynchronous onMount callback. It’s going to be a super short one!

As ever, you can refer to the GitHub repository for all of the code samples.

GitHub logo dirv / svelte-testing-demo

A demo repository for Svelte testing techniques

Important: What we’re about to do is something I don’t recommend doing in your production code. In the interests of simple testing, you should move all business logic out of components. In the next part we’ll look at a better way of achieving the same result.

Checking that a callback was called

Let’s started by defining the component. This is src/CallbackComponent.svelte:

<script>
  import { onMount } from "svelte";

  let price = '';

  onMount(async () => {
    const response = await window.fetch("/price", { method: "GET" });
    if (response.ok) {
      const data = await response.json();
      price = data.price;
    }
  });

</script>

<p>The price is: ${price}</p>
Enter fullscreen mode Exit fullscreen mode

To test this, we’re going to stub out window.fetch. Jasmine has in-built spy functionality—the spyOn function—which is essentially the same as Jest’s spyOn function. If you’re using Mocha I suggest using the sinon library (which, by the way, has really fantastic documentation on test doubles in general).

When I mock out the fetch API I always like to use this helper function:

const fetchOkResponse = data =>
  Promise.resolve({ ok: true, json: () => Promise.resolve(data) });
Enter fullscreen mode Exit fullscreen mode

You can define a fetchErrorResponse function in the same way, although I’m going to skip that for this post.

You can use this to set up a stub for a call to window.fetch like this:

spyOn(window, "fetch")
  .and.returnValue(fetchOkResponse({ /* ... your data here ... */}));
Enter fullscreen mode Exit fullscreen mode

Once that’s in place, you’re safe to write a unit test that doesn’t make a real network request. Instead it just returns the stubbed value.

Let’s put that together and look at the first test in spec/CallbackComponent.spec.js.

import { mount, asSvelteComponent } from "./support/svelte.js";
import CallbackComponent from "../src/CallbackComponent.svelte";

const fetchOkResponse = data =>
  Promise.resolve({ ok: true, json: () => Promise.resolve(data) });

describe(CallbackComponent.name, () => {
  asSvelteComponent();

  beforeEach(() => {
    global.window = {};
    global.window.fetch = () => ({});
    spyOn(window, "fetch")
      .and.returnValue(fetchOkResponse({ price: 99.99 }));
  });

  it("makes a GET request to /price", () => {
    mount(CallbackComponent);
    expect(window.fetch).toHaveBeenCalledWith("/price", { method: "GET" });
  });
});
Enter fullscreen mode Exit fullscreen mode

Beyond the set up of the spy, there are a couple more important points to take in:

  1. I’ve set values of global.window and global.window.fetch before I call spyOn. That’s because spyOn will complain if the function you’re trying to spy on doesn’t already exist. window.fetch does not exist in the Node environment so we need to add it. Another approach is to use a fetch polyfill.
  2. I do not need to use await any promise here. That’s because we don’t care about the result of the call in this test--we only care about the invocation itself.

For the second test, we will need to wait for the promise to complete. Thankfully, Svelte provides a tick function we can use for that:

import { tick } from "svelte";

it("sets the price when API returned", async () => {
  mount(CallbackComponent);
  await tick();
  await tick();
  expect(container.textContent).toContain("The price is: $99.99");
});
Enter fullscreen mode Exit fullscreen mode

Complex unit tests are telling you something: improve your design!

A problem with this code is that is mixing promise resolution with the rendering of the UI. Although I’ve made these two tests look fairly trivial, in reality there are two ideas in tension: the retrieval of data via a network and the DOM rendering. I much prefer to split all ”business logic” out of my UI components and put it somewhere else.

It’s even questionable if the trigger for pulling data should be the component mounting. Isn’t your application’s workflow driven by something else other than a component? For example, perhaps it’s the page load event, or a user submitting a form? If the trigger isn’t the component, then it doesn’t need an onMount call at all.

In the next part, we’ll look at how we can move this logic to a Svelte store, and how we can test components that subscribe to a store.

💖 💪 🙅 🚩
d_ir

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

Sign up to receive the latest update from our blog.

Related