Testing a Vue component part 4: more testing and faking time
Welcome to part 4 of a series on how to test a Vue component.
Here's the table of contents of the series:
- Starting out with tests
- The first test and snapshot testing
- Creating an instance of a component
- More tests and faking time (you're here)
- Jest awesomeness and a skeleton to get started More tests and faking time
More tests
If you navigate to http://vue-tabs-component.spatie.be/#second-tab you'll notice that the tab named Second tab
is automatically opened. Let's take a look at the test that makes sure this functionality works.
it('uses the fragment of the url to determine which tab to open', async () => {
window.location.hash = '#second-tab';
await createVm();
expect(document.body.innerHTML).toMatchSnapshot();
});
Let's go through it in detail. First we set the hash
of the current window.location
(that's JavaScript speak for the current url) to #second-tab
. Remember, Jest comes with jsdom out of the box. It's jsdom that provides us with that window
instance.
Next we create a new Vue instance and await
until Vue has done it's thing. Finally we assert that the rendered output matches the saved snapshot. Of course I've manually verified, when that snapshot was first created, that it's content was correct.
Asserting against a component property
Instead of using snapshots to make assertion you can also just assert against a property in the state of a Vue component. Let's see how that works.
Our vue-tabs-component has a neat feature where you can make a tab use a custom fragment. Here's the relevant test.
it('uses a custom fragment', async () => {
document.body.innerHTML = `
<div id="app">
<tabs cache-lifetime="10">
<tab id="my-fragment" name="First tab" >
First tab content
</tab>
</tabs>
</div>
`;
const tabs = await createVm();
expect(tabs.activeTabHash).toEqual('#my-fragment');
});
First we set up some html and we await
until Vue has done it's thing. Like mentioned before the createVm
method return an instance of the Tabs
component. We implemented our component in such a way that the activeTabHash
-property in it' state always contains the hash of the tab that's being displayed. In that last line of the test we simply assert that the property contains the expected value.
Faking time
By default our tabs component will remember which was the last open tab. If you for instance navigate to http://vue-tabs-component.spatie.be/#second-tab
and then to the same url without the fragment you'll notice that the Second tab
is being displayed.
The component remembers, for a limited amount of time, the last opened tab by writing the hash
of the selected tab to local storage. Here's the code that does that:
expiringStorage.set(this.storageKey, selectedTab.hash, this.cacheLifetime);
That expiringStorage class is something custom that is included in the project. It's a wrapper around local storage. It makes items in local storage expiring.
expiringStorage
has it's own tests. Let's take a look one of those tests.
it('remembers values by key', async () => {
expiringStorage.set('my-key', 'my-value', 5);
expect(expiringStorage.get('my-key')).toEqual('my-value');
});
Pretty simple right? We store my-value
in the storage using the key my-key
and then we assert that we get the same value when getting my-key
. That last parameter is the lifetime in minutes.
Of course we should not only test that the value is being saved, but that is expires after five minutes.
Here's the test for that:
it('returns null if the value has expired ', () => {
expiringStorage.set('my-key', 'my-value', 5);
progressTime(5);
expect(expiringStorage.get('my-key')).toEqual('my-value');
progressTime(1);
expect(expiringStorage.get('my-key')).toBeNull();
});
We assert that after exactly 5 minutes we still get the right value. If time progresses any further the value is forgotten and null
is returned.
That progressTime()
function is not part of Jest. It's coded up in the tests themselves. Let's go take a look at it's implementation.
function progressTime(minutes) {
const currentTime = (new Date()).getTime();
const newTime = new Date(currentTime + (minutes * 60000));
const originalDateClass = Date;
Date = function (dateString) {
return new originalDateClass(dateString || newTime.toISOString());
};
}
We'll go over this line by line. In JavaScript a way of getting the time is the built-in Date
class.
const currentTime = (new Date()).getTime();
We calculate the new time by adding the right amount of milliseconds to it.
const newTime = new Date(currentTime + (minutes * 60000));
Here comes the tricky part. We here going to hold a reference to the original date class. And then we are going to replace the built-in Date
with our own function.
const originalDateClass = Date;
Date = function (dateString) {
return new originalDateClass(dateString || newTime.toISOString());
};
This is called monkey patching. The original Date
is replace by a function that, given no parameters, it won't return the actual time, but the time we gave it (newTime
). Cool! Btw you can't do it this in PHP, which is a shame because it would make testing stuff a lot easier.
Now that you understand how progressTime
works let take a look at these two tests from inside tabs.test.js
.
it('opens up the tabname found in storage', async () => {
expiringStorage.set('vue-tabs-component.cache.blank', '#third-tab', 5);
const tabs = await createVm();
expect(tabs.activeTabHash).toEqual('#third-tab');
});
it('will not use the tab in storage if it has expired', async () => {
expiringStorage.set('vue-tabs-component.cache.blank', '#third-tab', 5);
progressTime(6);
const tabs = await createVm();
expect(tabs.activeTabHash).toEqual('#first-tab');
});
In the first test we set a value in the expiring storage with a key we know our component will use. Then we let Vue do it's thing. When it's ready we assert that the right tab is active.
In the second test we set the same value in the expiring storage, but then we are going to progress time. Finally we assert that the item in expiringStorage is not being used, but that the first tab is being used.
Now you might think that the that last test is a bit unnecessary. To so degree you're right: with the test of the expiringStorage
class itself we already covered the items do in fact expire. But that extra extra component test gives me some extra confidence that the component behaves correctly.
You've reached the end of part four. Only one more part to go: Jest awesomeness and skeleton to get started
What are your thoughts on "Testing a Vue component part 4: more testing and faking time"?