Vitest In-Source Testing for SFC in Vue?
Tags: vue, vitest, sfc, webdev
Published: 2024-04-16 14:51:07

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](https://vuejs.org/guide/scaling-up/sfc.html#why-sfc). 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](https://vuejs.org/guide/scaling-up/sfc.html#why-sfc) 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:

```jsx
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](https://vitest.dev/) and [happy-dom](https://github.com/capricorn86/happy-dom) to the project by running:

```jsx
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:

```jsx
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:

```jsx
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:

```jsx
npm install --save-dev @types/jest
```

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

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

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

```jsx
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:

```jsx
npm run test
```

Now it will run a test file and should return a successful result. 

![Screenshot of passing tests, hooray!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c7q6awpqwayjxv9kj8pd.png)

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](https://vitest.dev/guide/in-source) 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](https://github.com/joranquinten/experiment-vue-pure-sfc/commit/d003e35d7e8e751675454e972e7a9ead8bb90f89). 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:

```jsx
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:

```jsx
<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:

```jsx
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:

```jsx
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:

```jsx
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?

![Your scientists were so preoccupied with whether or not they could that they didn't stop to think if the should](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2qjrj0alwndgag7el0f1.png)

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](https://vitest.dev/guide/in-source#notes) 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!