In-Source Tests with Vitest

Posted 8 months ago
Return to overview
Four different flasks

Vue adopted a Single File Component philosophy, which has some benefits over splitting concerns, which you can read up on in the official Vue Docs. From a SFC philosophy, you’d want everything that relates to your component in a Single File. So let’s explore this take with our component tests as well, because why would your tests be any different than your scripts, template or styles?

We’re going to leverage a feature that Vitest offers, out of the box, to a Vue example code base. Bear in mind that this approach would be applicable to other implementations that leverage Vitest just as easy. Also, this is a thought experiment.

Let’s set up a small Vite Vue project

For a TLDR; The code can be found on https://github.com/joranquinten/experiment-vue-pure-sfc if you’re only interested in the end result.

We’ll keep it simple, because it’s about exploring a concept. You can create a small boilerplate using the following command:

npm create vite@latest vue-pure-sfc

Be sure to select the Vue framework (or your framework of choice) and, well, the rest of the config is up to you. Running the "npm install" and "npm run dev" commands should at least net you with the default template.

Next we’ll install Vitest and happy-dom to the project by running:

npm install --save-dev vitest happy-dom

We’ll add a "vitest.config.ts" file with the following contents, where we merge the default Vite config and extend it with Vitest specific settings:

import { defineConfig, mergeConfig } from 'vitest/config' import viteConfig from './vite.config' export default mergeConfig(viteConfig, defineConfig({ test: { globals: true, environment: 'happy-dom', }, }))

For testing Vue specifically, we’ll install Test Utils:

npm install --save-dev @vue/test-utils @vitest/coverage-v8

And if you use Typescript and don’t wan’t your editor to squiggle about the "it" and "expect" missing from the types, just install the Jest types to your project:

npm install --save-dev @types/jest

We’ll add a testing script to our "package.json" to execute our tests with ease:

... scripts: { ... abbreviated "test": "vitest --dom --coverage", ... abbreviated } ...

Now we can run our tests from the terminal with the command:

npm run test

It will fail horribly, since we don’t have any tests. Yet!

A Counter Component

As usual when we want to showcase some interactive component, we’ll create a simple counter. There is a simple example even in the boilerplate, but for testing purposes, let’s create a new one. We’ll create a file in the components folder and name it "Counter.vue". It easiest if you grab the contents from the example repository: https://github.com/joranquinten/experiment-vue-pure-sfc/blob/master/src/components/Counter.vue

In the "./src/components/HelloWorld.vue" file we’ll add it to the template, just to quickly show you that it’s working, see: https://github.com/joranquinten/experiment-vue-pure-sfc/blob/master/src/components/HelloWorld.vue

The Counter accepts a minimum and maximum value and can be incremented, decremented or reset if it’s dirty. Simple enough.

We can also add our unit tests the usual way, in a separate file, such as "./src/tests/Counter.spec.ts" with testing methods that validate the features of the component: https://github.com/joranquinten/experiment-vue-pure-sfc/blob/master/src/tests/Counter.spec.ts

Having all this in place, we can check whether our component works by running our test command in the terminal:

npm run test

Now it will run a test file and should return a successful result, like shown in this screenshot below:

Screenshot Vitest

This is great! We have a component and it’s tested with full coverage. Neat! From SFC perspective, we have all the logic that belongs to the component in one place, right? Well, not the tests. So we could debate whether a unit test (or specification) is part of the component, but we can safely state that they are very closely coupled!

In-Source testing to the rescue!

With Vitests In-Source testing feature, we can actually achieve this! There’s a caveat though. The script setup notation basically encapsulates the defineComponent function. We want to do something extra here, so we’ll convert the script setup notation to the more explicit Composition API notation. We’ll first store our component in a variable before exporting it. The reason for that will become clear!

Obviously, we’ve already have our tests in place, so we can easily test our little refactor with our existing tests:

npm run test

Everything checks out. Now we have some wiggle room to include more scripts into our component, namely our testing scripts! 🤯

Before the end of the script tag, we can import our testing packages like so:

<script lang="ts"> // ...the whole bunch of Composition API code import { shallowMount } from "@vue/test-utils"; if (import.meta.vitest) { const { it, expect, describe } = import.meta.vitest; describe("Counter Pure SFC", (): void => { // ... Pay close attention here! 🧪 👀 } } </script>

Now we can move the contents of our "Counter.spec.ts" file from the "describe" block to the "describe" block in the Vue component and remove the "Counter.spec.ts" file altogether!

With a final tweak of the "vitest.config.ts", we can have it ingest and execute the test blocks in our ".vue" files as well:

import { defineConfig, mergeConfig } from 'vitest/config' import viteConfig from './vite.config' export default mergeConfig(viteConfig, defineConfig({ test: { globals: true, environment: 'happy-dom', includeSource: ['src/components/**/*.vue'], }, }))

Not on production!

And to make sure our test code doesn’t end up on production, we can update the "vite.config.ts" accordingly:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // <https://vitejs.dev/config/> export default defineConfig({ plugins: [vue()], define: { 'import.meta.vitest': 'undefined', }, })

By setting the "import.meta.vitest" to "undefined" it will collapse the if statement and remove it from the production build. 😇

Now, by running the command for testing:

npm run test

You get to execute the components specification from inside of the component!

Single File Components with embedded Tests?

So, let’s recap on this whole experiment, from SFC philosophy, would is make sense to embed your tests?

Well, no. Although theoretically a specification can be considered part of the component, it’s not part of the core feature that the component unlocks. While it can be helpful to have the docs available when refactoring a component, it decreases the overall readability of the component. In this case I’d say that clear concise component code is more valuable in your code base than being an SFC extremist.

Embedding the tests doubled the lines of code from 78 to 156!

That’s excluding the conversion from script setup notation to the more verbose composition API. And the tests aren’t even that extensive! 🙀

This was just a silly experiment, to see how In-Source testing capabilities would affect the way we think about components. There may even be valid use cases where this can be very helpful: for very complex utility functions it could add to the readability and understanding of said function. And, spoiler alert: the Vitest docs also do not recommend using this for Component testing.

If you are interested in the setup, have a look at the repository to run the code for yourself!

Return to overview