Daniel Irvine š³ļøāš
Posted on January 12, 2020
Weāve covered off the basics and itās time to get to the fun stuff.
In this part of the series, weāll move the fetch
call from the CallbackComponent
into a module that updates a Svelte store.
Then weāll use babel-plugin-rewire-exports to spy on our new store fetch call and check that onMount
calls it when the component mounts.
Finally, weāll send a store update to verify that our component is in fact responding to updates.
Refer to the GitHub repository for all of the code samples.
dirv / svelte-testing-demo
A demo repository for Svelte testing techniques
Extracting a price
store from CallbackComponent
The file src/stores/price.js
is as follows.
import { writable } from "svelte/store";
export const price = writable("");
export const fetch = async () => {
const response = await window.fetch("/price", { method: "GET" });
if (response.ok) {
const data = await response.json();
price.set(data.price);
}
};
Now src/CallbackComponent.js
can be updated to use that store:
<script>
import { onMount } from "svelte";
import { fetch as fetchPrice, price } from "./stores/price.js";
onMount(fetchPrice);
</script>
<p>The price is: ${$price}</p>
Here Iām using the auto-subscribe feature of Svelte. By referring to price
using the $
prefix, i.e. $price
, Svelte takes care of the subscription and unsubscription for me. I like that. š
This code is looking much better already, and if you run npm test spec/CallbackComponent.spec.js
youāll find the existing tests still work. In fact, some people might say that you can leave this here and not bother with refactoring the test. Well, not me, I donāt say that: I say we need to get things in order before some later time in the future when we add to price
and our CallbackComponent
tests break unexpectedly.
(By the way, in the last part I mentioned that the trigger for the price fetch may be better placed elsewhere, not in a Svelte component, but letās go with this approach for now as it allows me to neatly introduce mocks.)
Testing the store
The tests, in spec/stores/price.spec.js
, look like this:
import { tick } from "svelte";
import { get } from "svelte/store";
import { fetch, price } from "../../src/stores/price.js";
const fetchOkResponse = data =>
Promise.resolve({ ok: true, json: () => Promise.resolve(data) });
describe(fetch.name, () => {
beforeEach(() => {
global.window = {};
global.window.fetch = () => ({});
spyOn(window, "fetch")
.and.returnValue(fetchOkResponse({ price: 99.99 }));
price.set("");
});
it("makes a GET request to /price", () => {
fetch();
expect(window.fetch).toHaveBeenCalledWith("/price", { method: "GET" });
});
it("sets the price when API returned", async () => {
fetch();
await tick();
await tick();
expect(get(price)).toEqual(99.99);
});
});
This is all very similar to the previous tests for CallbackComponent
. The changes are:
- The call to
mount(CallbackComponent)
is replaced with a call tofetch()
- Rather than checking that the price is rendered in the DOM, we check that the value of the store is
99.99
. To do that, we use Svelte'sget
function. - Crucially, we have to reset the store in between tests. I do that by calling
price.set("");
in thebeforeEach block
.
Resetting store state between tests: a better approach
The problem with the above approach is that the initial store value, ""
, is stored in two places: once in src/stores/price.js
and once in its tests.
To fix that, we can create a reset
function in src/stores/price.js
:
const initialValue = "";
export const reset = () => price.set(initialValue);
export const price = writable(initialValue);
Now we can use reset
inside of the tests:
import { fetch, price, reset as resetPrice } from "../../src/stores/price.js";
...
describe(fetch.name, () => {
beforeEach(() => {
...
resetPrice();
});
});
You may have noticed that Iām quite fond of renaming imports--price
exports fetch
and reset
functions, but Iāve renamed them as fetchPrice
and resetPrice
, for clarity.
Rewriting the CallbackComponent
specs
Letās start with the easiest spec: we want to check that the component subscribes to updates, so we should mount the component and afterwards update the price of the component.
Any time a state change occurs in a component, I like to ensure I have two tests: one for the initial value, and one for the changed value.
In the next code sample, Iāve purposefully made life hard for us just to make a point. Since our code is still calling the real fetch
function in src/stores/price.js
, we still need to stub out window.fetch
. If we donāt, our tests will error. (Try it if you donāt believe me!)
import { tick } from "svelte";
import { mount, asSvelteComponent } from "./support/svelte.js";
import { price } from "../src/stores/price.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("displays the initial price", () => {
price.set(99.99);
mount(CallbackComponent);
expect(container.textContent).toContain("The price is: $99.99");
});
it("updates when the price changes", async () => {
mount(CallbackComponent);
price.set(123.45);
await tick();
expect(container.textContent).toContain("The price is: $123.45");
});
});
Concentrating on the two tests for a moment, you can see Iām using set
to set the price. In the second test, I do that after the component is mounted to ensure that the subscription is working correctly.
(I didnāt bother to write a test for the _un_subscribe behavior. If I chose to implement the production code with subscribe
instead, my test would still pass. But if I didnāt remember to unsubscribe, thereād be a memory leak in my component. Everyone has their limit to how strict theyāll be with their testing... I guess thatās mine!)
But what about all that irrelevant stub set up? The moment has finally arrived: itās time to rewire our fetch
function.
Replacing dependencies with rewire
When we stubbed out window.fetch
, we were lucky in that it was a global function. But now we have a dependency that we want to stub out which is a module export: fetch
, the export from src/stores/price.js
.
To stub that, we need to rewire the export.
I played around with a few different packages, including rollup-plugin-stub which would be ideal except the package has been archived (I donāt know why) and the interface isnāt as nice as the actual choice Iāve gone with, which is babel-plugin-rewire-exports.
(If you're interested in using rollup-plugin-stub, I suggest using my fork which has a couple of improvements.)
So first things first, this is a Babel plugin so we have to tell Rollup to load Babel. You need to install all of the following packages, all as dev dependencies:
@babel/core
rollup-plugin-babel
babel-plugin-rewire-exports
Then it gets enabled in rollup.test.config.js
:
import babel from "rollup-plugin-babel";
export default {
...
plugins: [
...,
babel({
extensions: [".js", ".svelte"],
plugins: ["rewire-exports"]
})
]
};
No Babel config is required--thatās all you need!
Letās get down to business. Weāre going to use jasmine.createSpy
to create a spy, rewire$fetch
to rewire the fetch call, and restore
to restore all the original function.
First up, the import for src/stores/price.js
changes to the following.
import {
price,
fetch as fetchPrice,
rewire$fetch,
restore
} from "../src/stores/price.js";
Itās the rewire plugin that provides the rewire$fetch
and restore
function. Weāll see how to use both in the next example. The most complex part is that rewire$fetch
operates only on fetch
, but restore
will restore all of the mocks from that module.
And then, the test:
describe(CallbackComponent.name, () => {
asSvelteComponent();
beforeEach(() => {
rewire$fetch(jasmine.createSpy());
});
afterEach(() => {
restore();
});
// ... previous two tests ...
it("fetches prices on mount", () => {
mount(CallbackComponent);
expect(fetchPrice).toHaveBeenCalled();
});
});
The test itself is very simple, which is great. And we can delete the existing stubbing of window.fetch
, which is good because the component doesnāt make any reference to window.fetch
.
In the next part weāll continue our use of mocking but extend it to mocking components themselves, for which weāll need a component double.
Posted on January 12, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.