Vitest In-Source Testing for SFC in Vue?

Vitest In-Source Testing for SFC in Vue?

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. Vitest docs officially do not recommend this approach, but I think it’s an interesting approach to investigate.

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 vuepuresfc

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 savedev vitest happydom

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 savedev @vue/test-utils @vitest/coveragev8

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 savedev @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 ./src/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.

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!

Leave a Reply

Your email address will not be published. Required fields are marked *