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.
Top comments (14)
Can you please offer some insight into how to make the localStorage available on the global window?
I import stores into my components, but they are failing because the localstorage that hydrates the store is not available on import.
I tried creating a localStorage object from here stackoverflow.com/a/26177872/6130344 and added it to global.window.localStorage in setupGlobalJsdom() with no luck :(
I see this is available, npmjs.com/package/jasmine-local-st..., but it looks like it's for Jasmine 2, and I'm not sure if importing it into svelte.js and adding it to global.window.localStorage is just the complete wrong way to go about this.
Any suggestion?
I knocked this up just now:
github.com/dirv/svelte-testing-dem...
Actually kind of tricky, because writing a store in the conventional Svelte way, by calling
writable
at the top-level, means that the store is instantiated as soon as you import the file.That means you need a
loadX
function so that you can separate out the definition of the store from the loading fromlocalStorage
. This function would need to be called somewhere when your application loads, which I havenβt shown.That means you need separate values for
notLoaded
andnotLoggedIn
, in this example.Also, Svelte
subscribe
calls the callback immediately on definition, with whatever value the store currently has.Also, I donβt call
unsubcribe
anywhere... which is probably fine...subscribe
was likely never designed to be used outside of a component.Another way to do this: I could have called
setItem
directly after callinguser.set
rather than usingsubscribe
.Btw, this doesnβt answer the question of how to set up local storage for a component that uses this store. I havenβt tried it but Iβd hope in that case you could call JSDOMβs non-stubbed
getItem
and then ensure thatloadUser
is called in yourbeforeEach
.Let me know if this helps!
I appreciate you taking the time, I really hope this helps someone!
Here is a link to one of my custom stores:
github.com/Lamden/wallet/blob/mast...
I like them because they self hydrate from the localstorage. Also,, the value that is sent first is the correct one and not a placeholder; which means my components don't need to handle an empty value. I also don't need to call an initiate function anywhere in my app.
You are right that it seems there is no way around this in testing. I see what you have done with the user store and I like it as it works great for testing, but doesn't work for my situation as it seems localstorage doesn't get added to the window till later.
My app is a Chrome Extension, so I'm 100% sure the window will be there for my store when the app loads.
This may be one of those situations where I have to resort to manual testing!
Another thing you can try is instantiating JSDOM before you import any test files. For Jasmine, you can create a helper (which is simply an file thatβs loaded) and register it in
spec/support/jasmine.json
.Jest does this implicitly because it has the notion of an βenvironmentβ in which your tests run, and the standard environment is a JSDOM environment.
I donβt really like this approach as not all test files require JSDOM, but it might solve this problem? Itβs worth trying.
Would I then import the JSDOM into svelte.js? helpers don't seem to be ES6 compatible so this is causing me some syntax errors.
Oh, of course. I should have thought of that. Youβll need to take that one function, create a new file with just that, and convert import -> require. I can try it myself tomorrow
What version of JSDOM are you using? The latest version has an implementation of localStorage ready to go, I think.
You can also use spies but I wouldnβt bother with using a package for it (although thatβs what I say about everything to be fair, so ymmv π€·ββοΈ)
Hereβs an example of how I used spies to test local storage in a React codebase (with Jest):
github.com/PacktPublishing/Masteri...
So now I need to learn Jest or does Jasmine have spies, also I guess I need to find out what spies are lol.
I'm using whatever JSDOM you had in your dependency tree. I copied them out.
Jasmine has spies - jasmine.createSpy
Iβll see if I can port across the local storage tests to Svelte a little later today.
Me again :)
What is the purpose of removing the stubbing and adding rewire$fetch?
There is now no way to mock the return value of fetch which I thought was the point.
I think I may be a bit lost.
I'm wondering if it's because you split the testing of the store out to it's own spec and you test the fetch in there. Then you don't need to retest that in the Component spec.. Is that correct?
Correct :)
π€£
I think I confused this by aliasing
fetch
asfetchPrice
, but leavingrewire$fetch
instead of naming thatrewire$fetchPrice
.Thanks for this feedback. I need to work on this a little!
Under "Rewriting the CallbackComponent specs" am I right to assume that the "displays the initial price" test is not correct as per the implementation? The initial price should be set by the fetch call (which is stubbed to 99.99). By setting the store value directly you are just duplicating the next test case making them redundant (if set works it works).
Should it be re-written like so as to test being set by fetch?
it("displays the initial price", async () => {
mount(CallbackComponent);
await tick();
expect(container.textContent).toContain("The price is: $99.99");
});
Also, I seem to need like 5 "ticks" or else the test case will fail. Any idea why? is there a danger in using too many ticks?
Thanks for pointing this out, I think I just did a poor job of explaining it so Iβll try to rewrite this to make it clearer.
This middle section is mid-refactor which is part of the trouble. Because I'm refactoring the code by extracting the fetch logic into a store, the tests for
CallbackComponent
should no longer care about fetch at all.The test
displays the initial price
that doesnβt care so much about how the data is retrieved, it just cares that the Svelte subscription forprice
is set up correctly.But the test with the set AND the fetch stub is a half-way house between the old version and the new version. The final section of this post removes the need to stub
window.fetch
by instead stubbing outfetchPrice
instead. That is a good thing because the tests forCallbackComponent
then have no knowledge of howfetchPrice
works (i.e. by callingwindow.fetch
, just like the component itself.Iβm still not sure if Iβm doing a good job of describing this--let me know if this has helped!