Joran Quinten
Posted on April 16, 2024
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 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 ./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!
Posted on April 16, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.