As we gain experience as a developer and requirements become more complex, testing provides us with a powerful tool to take firm steps forward. It also gives us security when we can verify that all tests still pass after making changes in our code.
In this post we are going to carry out a simple test as an exercise to review some basic concepts and, through a practical example, understand how to take advantage of testing utilities in case that our component requires opening a new tab using window.open
function.
You can find the code in this repository --> github
Disclaimer: This post is not about TDD. I just try to explain step by step what happens when someone tries to learn new concepts from practice and also show you the way i usually do it
Setting up our project
Let's start creating a new project with vite. We will name it vue-mocking-window-object.
yarn create vite
Select Vue and Typescript
Vite doesn't install dependencies so we will have to do it.
cd vue-mocking-window-object
yarn install
Now we have to add vitest, @vue/test-utils and typescript types to be used in our project.
yarn add -D vitest @vue/test-utils @types/jest
Note: -D is for installing libraries only as a development dependency
The next step is to add scripts to package.json so we can use them with yarn. Just edit package.json and add this
"scripts": {
"test": "vitest",
"coverage": "vitest run --coverage"
}
We have to modify vite's config to add testing configuration.
import { defineConfig, UserConfigExport } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
const config: UserConfigExport =
{
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
}
}
export default defineConfig(config)
By setting globals as true, it will inject jest into tests without needing to importing it.
environment is being used for configuring vitest to set a browser-like env for our tests, otherwise it will be a node.js env and some global object like document
won't be available in that case.
And that's all! Let's create a simple test to verify if everything is working as expected.
Creating our first test
Modify the file App.vue (see below) and delete file HelloComponent.vue from components folder.
<template>
<div>Welcome to Vue + Vitest</div>
</template>
Then, if we start development server with yarn dev
and open a browser with url http://localhost:5173
we should get this page.
Now create a file named App.spec.ts in same folder as App.vue and write the following code:
import { mount } from '@vue/test-utils';
import App from './App.vue';
describe('Testing APP component', () => {
it('render component', async () => {
const wrapper = mount(App);
expect(wrapper.text()).toContain('Welcome to Vue + Vitest');
})
})
Run in a terminal yarn test
and...
Yes!!! That's was easy right?
With help of vue test-utils we can render the component into a variable and use jest to create our validations. In this test we can verify that the string 'Welcome to Vue + Vitest' is being rendered by the component.
Ok, let's try something a little bit more complex.
Create base component
We want a component that renders an ethereum wallet address and by clicking on it let us open a new tab that shows the etherscan of that wallet. Etherscan.io is a public site where you can search and trace Ethereum transactions just like transfer, contracts interaction, etc...
For doing this (and just keeping this post simple) create a file named EthereumAddress.vue in components folder and write this code:
<script setup lang="ts">
type Props = {
address: string;
};
defineProps<Props>();
</script>
<template>
<div class="ethereum-address">{{ address }}</div>
</template>
<style>
.ethereum-address {
font-family: "Courier New";
}
</style>
Edit App.vue to import and mount this component and check what happens in browser
<script setup lang="ts">
import EthereumAddress from "./components/EthereumAddress.vue";
</script>
<template>
<div>Welcome to Vue + Vitest</div>
<EthereumAddress address="0x839Be166ee5Ac99309C0A0796867942822a5E8BE" />
</template>
And now we see this
Ok, great, it works. It's good time to create a test for our component.
Create a file named EthereumAddress.spec.ts and write this code:
import { mount } from '@vue/test-utils';
import EthereumAddress from './EthereumAddress.vue';
// Testing purpose ethereum address
const testingAddress = '0x839Be166ee5Ac99309C0A0796867942822a5E8BE';
describe('EthereumAddress Component', () => {
it('render address passed as prop', async () => {
// Render component
const wrapper = mount(EthereumAddress, {
props: {
address: testingAddress
}
});
// Verify
expect(wrapper.text()).toBe(testingAddress);
})
})
run yarn test
again and verify that all tests passed
Add functionality
We want to open etherscan when a user clicks on it. For making that happen we will open a new tab by using window.open
function.
We need base url of etherscan and, as a good practice, make all the global constants be imported from .env file because probably you will need different base urls for each environment (development, testing, production, etc...)
Create a .env file and write this
VITE_APP_ETHERSCAN_URL=https://etherscan.io/address/
Now create a file named constants.ts to import this var from .env. By using a single file to define all the constants is also a good practice to avoid importing from .env all over the app. It gives you more flexibility when something changes and adds an extra layer of validation like this.
export const ETHERSCAN_URL = process.env.VITE_APP_ETHERSCAN_URL;
if (!ETHERSCAN_URL) {
throw new Error('VITE_APP_ETHERSCAN_URL must be set on .env');
};
Modify EthereumAddress.vue file and write this code:
<script setup lang="ts">
import { ETHERSCAN_URL } from "../constants";
type Props = {
address: string;
};
const props = defineProps<Props>();
const handleEtherscan = () => {
window.open(`${ETHERSCAN_URL}${props.address}`, "_blank");
};
</script>
<template>
<div
class="ethereum-address"
@click="handleEtherscan"
data-testid="ethereumAddress"
>
{{ address }}
</div>
</template>
<style>
.ethereum-address {
font-family: "Courier New";
cursor: pointer;
}
</style>
Notice that we add data-testid
property in div component, it makes easy to "find" the component on our tests
Let's modify EthereumAddress.spec.ts by adding a test which makes a click on the component and see what happens
import { mount } from '@vue/test-utils';
import EthereumAddress from './EthereumAddress.vue';
// Testing purpose ethereum address
const testingAddress = '0x839Be166ee5Ac99309C0A0796867942822a5E8BE';
// testid reference tag
const referenceLabel = '[data-testid="ethereumAddress"]';
describe('EthereumAddress Component', () => {
it('render address passed as prop', async () => {
// Render component
const wrapper = mount(EthereumAddress, {
props: {
address: testingAddress
}
});
// Verify
expect(wrapper.text()).toBe(testingAddress);
})
it('click address and verify if a new tab was opened', async () => {
const wrapper = mount(EthereumAddress, {
props: {
address: testingAddress
}
});
wrapper.find(referenceLabel).trigger('click')
})
})
Probably you must see something like this...
What this means is that in the testing environment window.open
doesn't exists, because it says it's not implemented.
Okay, now what?
We just want to know if window.open
was called with etherscan url and _blank as parameters and we don't care what this function does in the browser. What we are going to do is to use a tool called spy to verify if the function was called or not. Vitest provides this functionality!
Modify the test like this
import { mount } from '@vue/test-utils';
import { vi } from 'vitest';
import EthereumAddress from './EthereumAddress.vue';
// Testing purpose ethereum address
const testingAddress = '0x839Be166ee5Ac99309C0A0796867942822a5E8BE';
// testid reference tag
const referenceLabel = '[data-testid="ethereumAddress"]';
// Etherscan url from environmend
const ETHERSCAN_URL = import.meta.env.VITE_APP_ETHERSCAN_URL;
describe('EthereumAddress Component', () => {
it('render address passed as prop', async () => {
// Render component
const wrapper = mount(EthereumAddress, {
props: {
address: testingAddress
}
});
// Verify
expect(wrapper.text()).toBe(testingAddress);
})
it('click address and verify if a new tab was opened', async () => {
// Render component
const wrapper = mount(EthereumAddress, {
props: {
address: testingAddress
}
});
// Create spy for testing if window.open was called
const spy = vi.spyOn(window, 'open');
// Act: click on the address
wrapper.find(referenceLabel).trigger('click')
// Verify if window.open was called
expect(spy).toBeCalledWith(`${ETHERSCAN_URL}${testingAddress}`, '_blank');
})
})
Run tests! now must be ok!
Well, our tests pass but what is that!?!?
window.open still throw error... ummm
What happened here was that the function window.open
was correctly called, with correct parameters but the function actually doesn't exists!
We can use another testing tool to create a global function named window.open to avoid this error. It's named stub. A stub is a piece of code used to substitute another functionality just for testing purposes and vitest provides us with this tool.
Add this line of code at the beginning of test after describe
// Mock global window.open
vi.stubGlobal('open', vi.fn());
And now everything is fine, our tests passed and no errors were shown. Congratulations!
Top comments (0)