Shared testing configuration with Jest and Vitest
This lesson preview is part of the Bundling and Automation in Monorepos course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Bundling and Automation in Monorepos, plus 90+ \newline books, guides and courses with the \newline Pro subscription.

[00:00 - 00:09] In this lesson, we're going to set up optimized testing tools with a common config. We're going to do this for Jest because it's an industry standard.
[00:10 - 00:25] And we're also going to do it for Vitest, which is a newer and better test runner. My personal recommendation is to always use Vitest for new projects, and to convert whatever old projects you have to Vitest if possible.
[00:26 - 00:43] However, I do understand that that's not always possible. So we're going to see an optimized configuration for Jest that deals with a lot of its quirks. I'm going to go to our Monorepo packages config workspace, and then I'm going to add the necessary dependencies for Jest.
[00:44 - 01:06] We're going to be using SWC as our transpiler because Jest cannot handle TypeScript by default. There are other transpilers compatible with Jest, but SWC has the best performance and feature set. I'm going to create a file called jest.base.mjs, and I'm going to paste in a config I prepared ahead of time.
[01:07 - 01:23] I'm using a type comment to add type information to our JavaScript file, which helps with editor autocomplete and option documentation. An important option to point out is that we need to disable Prettier by setting "prettierPath" to undefined.
[01:24 - 01:37] The reason is that the current version of Jest, version 29, still does not support Prettier version three, even though it was released more than a year ago. The next important configuration is our transform configuration.
[01:38 - 01:49] This tells Jest to run files that match our glob through the @swc/jest transpiler. This is what allows Jest to run on TypeScript files.
[01:50 - 02:13] Next, we have a "maxWorkers" configuration that allows you to either set workers by using an environment variable, or it sets an optimized value based on the current environment. For CI runs, it's going to use one worker, while for local development it's going to default to 50% of your CPU cores. Finally, we just add an ignore of the dist folder.
[02:14 - 02:29] By default. This is enough for our base config, and I'm now going to go to our frontend-nextjs application. I'm going to start by creating our test file, and then we'll make it work step by step, fixing the problems along the way.
[02:30 - 02:53] The point of this lesson is just to make sure that the test runner runs correctly, so the test we're going to do just makes sure that we can correctly parse TSX into Jest. The first error that we're going to see is that the "test" and the "expect" methods are not defined. This error is coming from TypeScript, and the reason is that TypeScript thinks no such methods are globally defined.
[02:54 - 03:06] In practice, the Jest test runner defines them when it executes your test files. The fix is easy enough: we need to add the @types/jest package which defines these methods globally.
[03:07 - 03:17] This, however, is not good practice. Now the methods are valid everywhere in the app, even in the browser code, and TypeScript will not warn us if we accidentally use them in the wrong place.
[03:18 - 03:29] We'll see a better approach later, but for now, let's go and create a script for running our tests, which is simply going to be running the Jest command. And then let's try running the script.
[03:30 - 03:46] It's going to fail because we haven't installed the Jest package here yet, so let's do that and then try running the tests again. Now we can see that Jest does not support TypeScript or ECMAScript for that matter, and it cannot import our file.
[03:47 - 04:08] This is why we needed to set up SWC as a transplant in our common config. So let's bring that config into this application, I'm going to create a jest.config.mjs file, and I'm going to import from our @monorepo/config/jest.base.mjs file.
[04:09 - 04:26] We can directly re-export that configuration and try running the test again and see that it's going to pass. We now have a working configuration, but I'm just going to make it a bit easier to extend in the future. I'm going to expand our base configuration into a variable called "config".
[04:27 - 04:38] I'm going to set its TypeScript types correctly, and then I'm going to re-export it. So in the future when we need to change anything here, it's easy, and we will have editor autocompletion.
[04:39 - 04:54] Looking at the total changes we made, we added the test script that runs Jest, we added our dependencies on @type/jest and Jest itself. And then we wrote our very small extended config and test file.
[04:55 - 05:11] With this done, I'm going to go to our frontend-vite application then I'm going to create a test file in the src directory called app.test.tsx. I'm going to paste in the code that I prepare ahead of time, and we're going to see that we're missing the "vitest" import.
[05:12 - 05:27] I'm going to add Vitest as a dependency, and then I'm going to create our test script which is going to execute "vitest --no-watch" option. Dropping down to the command line, we can now see that our test script runs.
[05:28 - 05:46] I want to point out that this works without us having to do any external configuration. And because we import the "test" and "expect" functions from Vitest, our source code is going to be safer compared to with Jest, where we could accidentally use those functions in an inappropriate place.
[05:47 - 06:17] However, the point here is to create shared configuration and I'm unhappy with the "--no-watch" option that we needed to manually add to our test script. So I'm going to go to our @monorepo/config package and I'm going to create vitest.base.mjs. I'm going to paste in some code that I've prepared ahead of time, and we're going to just change a couple of defaults that make Vitest execute faster.
[06:18 - 06:32] By default we're going to set that we start in single run mode rather than watch mode. We're going to run with "isolate: false" because it drastically improves performance and is safe for almost all code bases.
[06:33 - 06:45] And we're just going to add "dist" as a default exclude folder. I'm going to add Vitest as a dependency to our config package, and then I'm going to go back to our frontend-vite application.
[06:46 - 07:02] I'm going to create a file called vitest.config.ts, and I'm going to paste in again something that I've prepared ahead of time. In a Vite application, we're going to need to extend our default Vite config as well as our base config that we just created.
[07:03 - 07:10] So to do that I'm going to be using the Vitest "mergeConfig" function. Additionally, we need to update our TypeScript configuration.
[07:11 - 07:27] Vite uses a composite TypeScript project, and we need to add our new config file to the Node TSConfig, as well as allow extensions in imports. With these changes, our config file now passes TypeScript.
[07:28 - 07:37] Additionally, if we just run Vitest, we'll see that we no longer need the "--no-watch" option. It's part of our default config. So I'm going to go to package.json and remove it from there.
[07:38 - 07:44] Verify that it works, and it does. The config that we needed to add here was fairly simple.
[07:45 - 08:09] Add "vitest", set up our config file that extends from our base config, and write a small test file to verify that everything works. Finally, I'm going to go to our server-express app, and even though we don't use Vite here, we're going to still use Vitest for our testing. I'm going to again create a very simple test file that just makes sure that imports work correctly.
[08:10 - 08:28] I need to go to my source code and export the "createServer()" function, and then we can go to the process of making Vitest work. I'm going to add "vitest" as a dev dependency and then try running "pnpm vitest --no-watch". We see that this works correctly even without any config.
[08:29 - 08:54] The Vitest has pretty good defaults, but I'm still going to create a local config file. I've prepared this ahead of time and we could have directly exported our base config file, but I've shown you how you can extend your config here as needed. Finally, I'm just going to add a "test" script to package.json that runs Vitest and verify that this works.
[08:55 - 09:23] I'm then going to go to our config package, and I want to see our base configurations side by side. It's worth mentioning that there are other ways we could have done this configuration. We could have used Jest presets, we could have used JSON config files, or we could have gone for different types of Jest wrappers like ts-jest. However, I prefer this approach because it's a JavaScript file.
[09:24 - 09:31] It uses the normal import rules. It can be verified by other tools and it has type information.
[09:32 - 09:48] Finally, I'm going to go back to the Monorepo root, open package.json, and then create a common test script that runs for all workspaces. Write that, run it, and we can see that all our tests pass as expected.