DEV Community

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.

Top comments (0)