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.
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>
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) });
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 ... */}));
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" });
});
});
Beyond the set up of the spy, there are a couple more important points to take in:
- Iâve set values of
global.window
andglobal.window.fetch
before I callspyOn
. Thatâs becausespyOn
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 afetch
polyfill. - 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");
});
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.