Unit testing for Vue.js and TypeScript revisited
Alternative set-up for unit testing Vue.js and TypeScript code using Jest
About a month ago, I’ve written about unit testing for Vue.js and TypeScript that involved Karma and Jasmine. The premise was that you could use webpack to compile .vue
files, which was not trivial to pull off in other test frameworks. This still holds true, but I’ve since changed my mind about the usage of .vue
files altogether.
In this article, I’ll go into why I’ve changed my mind about .vue
files, and why I think Jasmine simply does not cut it (as one of the readers pointed out). Sometimes I just have to be stubborn and learn things on my own. Without even trying, I would not have a good argument about why one should avoid using Jasmine.
Why .vue files were not so nice after all
There is absolutely nothing wrong with .vue
files. They do what the box says they would. Same goes for any container format (e.g., Riot). The main problem is that these files are not standard. They are completely custom formats for which the support has to be provided by various frameworks. .vue
is supported by Webpack, but not supported by Jest, Ava, etc, for example.
For me, the main reason .vue
files stopped being cool was Jasmine. As explained in my previous article on testing, options other than Karma + Jasmine were a no-go for testing when .vue
files are involved. Since Jasmine itself was a no-go, .vue
files become a no-go as well.
Beyond .vue files
Vue.js provides 3 ways to render templates. The .vue
files, string templates, and render functions. If you’ve been following my writing, you know that I’ve written about using JSX with Vue.js. This is currently the options of my choice. I’ve written about what JSX is, and how it relates to render functions, but I haven’t written why one would like to use it.
The two main reasons I chose JSX is that:
- It’s sugar-coated bare metal code, so it will be more widely supported and faster
- It doesn’t require me to ship the template compiler with the build
Finally, using JSX (or plain render functions, which is the same thing) makes the code readily testable, because it’s just plain TypeScript, which is well-supported by a number of test frameworks including Jest.
Why Jest
As with anything that comes from Facebook these days, Jest is seriously cool. It requires far less configuration than any other framework I’ve used (provided you are not using exotic features like the .vue
files), and it’s stupendously fast.
The main reason I use it, though, is that it has powerful mocking facilities. Writing testable code is fine. It’s a goal to strive for. In some situations, though, it does not seem to work out for me.
Sometimes, writing testable code requires me to add a lot of boilerplate. In fact, this boilerplate code would not even be noticeable if you test-first it, but if you back-test like I do, you will notice it when you start refactoring to make the code testable. In some instances, I would rather just mock the dependency than inject it, and thus reduce the boilerplate and API surface.
The way Jest makes mocking so easy, greatly reduces the effort required to test the code, sometimes completely doing away with the need to make it testable first.
Don’t get me wrong. I’m not saying that you should not write testable code. That should always be the top priority. I’m just saying that mocking should be taken into account when talking about testable code. If it can be tested with mocks, it is testable.
Configuring Jest to work with TypeScript
First thing first, you’ll need to install a few dependencies:
yarn add -D jest @types/jest ts-jest
If you are no familiar with how Jest works, let me first explain that. Jest works with CommonJS modules, and runs the tests on NodeJS. It uses jsdom to simulate the DOM. It also uses Babel, to provide native ES6 support.
For non-JavaScript modules like CSS, TypeScript, and anything else that you might be using in Webpack-based builds, Jest uses preprocessors. These preprocessors are not available for everything, though. One of these things is, you’ve guessed it, .vue
files. TypeScript, on the other hand, is well-supported. The ts-jest preprocessor is what we use in our example.
Jest is configured using package.json
. It has its own key, aptly named "jest"
which houses its options. Ours will look like this:
{
...
"jest": {
"transform": {
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testRegex": "\\.test\\.tsx?$",
"modulePaths": [
"<rootDir>/src/",
"<rootDir>/node_modules/"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js"
]
}
}
The "transform"
key specifies that we want to use the ts-jest preprocessor to compile files with .ts
and .tsx
extensions. I use JSX for Vue.js components, so all I need is to compile plain .tsx
files, which is supported by TypeScript without additional dependencies.
The "testRegex"
key tells Jest to look for any files that end in .test.ts
or .test.tsx
. This lets us put test files in the same directory as the module being tested. Putting tests in a single directory has the benefit of being able to get a quick glance at all of our tests, and peruse it as documentation. On the other hand, having tests alongside our tested modules has the benefit of more flexibility when moving things around, and less hassle when setting webpack up. I usually go for the latter.
I won’t go into the details of the "modulePaths"
setting. I’ve written an entire article specifically on that topic. I’m simply mentioning it here so you know it exists in case you haven’t considered the set-up discussed in that article.
Finally, the important bit. The "moduleFileExtensions"
key provides a list of module extensions. It’s important to ensure js
is listed in there. Without it, Jest will not be able to resolve any non-TypeScript libraries you may reference.
There are, of course, other things to worry about, like CSS modules, and similar, which also require a preprocessor. Refer to the Jest documentation for more on that.
Conclusion
By ‘downgrading’ your experience from .vue
files to plain TypeScript modules, things become dramatically simpler in terms of builds, and test runner configuration. At the same time, it widens your choices when it comes to tooling, and allows you to harness modern tech with all their bell-whistly goodness.
In software engineering, some trade-offs have to be made sometimes — at times, you have to sacrifice one thing to get another. This is a good example of those times. For me, at least, this was a worthwhile sacrifice, and I can’t say I’m missing the .vue
files a whole lot. (There is a secret sauce, though, that makes this even nicer than you might think on the first try. Hopefully I will be able to write about it at some point.)
I’m having a rather lazy weekend today, so no code samples. It’s also all rather straightforward, so I don’t feel the urgency to provide them either.