Setting up a Next.js Application with TypeScript, JIT Tailwind CSS and Jest/react-testing-library
Tags: nextjs, typescript, jest, tailwindcss
Published: 2021-08-06 02:15:47

A few weeks ago, I tweeted about how frustrating of an experience is to set up a Next.js project that includes TypeScript as well as a working testing framework.

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Setting up a Next.js TS project with Jest and RTL is beyond frustrating. <br>I really can&#39;t blame folks who are scared to get into the JS world. It really makes no sense how much confusing extra work you need just to make a testing framework work. I lost hours and got nowhere.</p>&mdash; Antonio Lo Fiego ☁️ (he/him) (@antonio_lofiego) <a href="https://twitter.com/antonio_lofiego/status/1412102191528153092?ref_src=twsrc%5Etfw">July 5, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
I tend to use `create-next-app` over `create-react-app` because Next is such a pleasure to work with, thanks to its extremely intuitive file-based routing, support for server-side rendering, static site generation and incremental site generation, wonderful components such as the `Image` optimization and an overall wonderful DX.

Something that `create-next-app` has been lacking, though, is a single source-of-truth when it comes to setting up testing environments. `CRA` ships with [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) out of the box and there's no major tweaking needed to start working on a project using TDD. The Next.js docs are wonderful, but nowhere they mention testing.

Moreover, Next makes it so easy and straightforward to use TypeScript. You could just run `yarn create next-app --typescript new-project` and all the setup is done for you. One feature I absolutely love about TypeScript are [path aliases](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping), as they make it so easy to work with larger React projects without having to deal with a jungle of `../../../../`s. While adding TypeScript to Next is nice and easy, it just adds more complexity when trying to set it up with Jest and RTL. 

Even more headaches are added if we want to include Tailwind CSS, arguably the best CSS framework out there right now. With their newly released JIT compiler, it has become such a pleasure to style your apps without writing a single line of traditional CSS, but setting it up along the rest of the tools is also another head scratcher.

After banging my head on this for quite some time, I finally put together a solid template that you can use to start your Next project with this wonderful stack and in this article I'll walk you through how I did it, so you can understand where certain complexities arose.

## create-next-app

The first step is to get a boilerplate Next app using `create-next-app` with the `--typescript` flag. That will take care of all the TS-related dependencies and configurations and get us started with the initial bits of components.

```{bash}
$ yarn create next-app --typescript new-project
```

## Path Aliases

As mentioned, I love using TS path aliases in my React projects, as it means that whenever I am building my `pages` I can just import my components from `@components` instead of manually writing relative imports.

To do this, we can open our `tsconfig.json` file, and add a couple of extra lines:
```{json}
{
	"compilerOptions": {
		{...}
        "baseUrl": ".",
		"paths": {
			"@components/*": ["components/*"],
			"@styles/*": ["styles/*"],
			"@pages/*": ["pages/*"],
			"@hooks/*": ["hooks/*"]
		}
    {...}
}
```


Note that these are the shortcuts I use, but you can change the actual path alias to whatever you'd prefer (E.g: `@/Components/*` or `#components/`).

## Tailwind CSS and JIT Mode

We can move on to Tailwind CSS. I specifically want to enable the JIT compiler as it works wonders and makes my CSS development so much smoother.

First of all, let's install the dependencies:

```{bash}
$ yarn add -D tailwindcss@latest postcss@latest autoprefixer@latest postcss-cli
```

After that, we can run `npx tailwindcss init -p` to get an empty Tailwind CSS config file as well as the appropriate PostCSS configuration. Given that we are using JIT, we don't specifically need to add the folders to purge in the configuration, but in case we decide to stop using JIT, this will take care of the production build. Our `tailwind.config.js` file should look like this:

```{js}
module.exports = {
	mode: 'jit',
	purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
	darkMode: false, // or 'media' or 'class'
	theme: {
		extend: {},
	},
	variants: {
		extend: {},
	},
	plugins: [],
};
```

In the `styles` folder that `create-next-app` generated for us, we can get rid of the `Home.module.css` file and clear the `globals.css` file. We will add a `tailwind.css` file which will only contain the Tailwind directives:

```{css}
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Finally, we need to add two scripts to our `package.json` that will take care of building new classes as we work in our application as well as building the final CSS package.

```{json}
{
    {...}
	"scripts": {
		"dev": "next dev",
		"build": "next build",
		"start": "next start",
		"lint": "next lint",
		"css:dev": "TAILWIND_MODE=watch postcss ./styles/tailwind.css -o ./styles/globals.css --watch",
		"css:build": "postcss ./styles/tailwind.css -o ./styles/globals.css",
	},
    {...}
```

The `css:dev` script will run PostCSS in watch mode, and Tailwind will listen for changes in our component classes to build new utilities classes on the go. Once everything is done, we can build the final version of the CSS by running our `css:build` script.

> **NOTE**: On Windows, the `dev` script might not work because of how environment variables are declared on Windows. A simple solution is to add another package called `cross-env`, which handles for you all the platform idiosyncrasies while setting environment variables. Just add `cross-env` before `TAILWIND_MODE` and you're all set!

Let's now test if Tailwind works properly. Replace the content of your `pages/index.tsx` file with:

```{tsx}
const Home = () => {
	return (
		<>
			<main>
				<div className='h-[100vh] flex flex-col justify-center align-middle text-center'>
					<h1 className='text-[72px] bg-clip-text text-transparent bg-gradient-to-r from-green-400 to-blue-500 font-extrabold'>
						Batteries Included Next.js
					</h1>
					<h2 className='text-2xl max-w-md mx-auto'>
						A Next.js Boilerplate with TypeScript, Tailwind CSS and testing
						suite enabled
					</h2>
				</div>
			</main>
		</>
	);
}

export default Home;
```

Now, in two terminal windows, run both `yarn dev` and `yarn css:dev` and on http://localhost:3000 you should see:
![A working Next.js page with Tailwind CSS](https://cdn.hashnode.com/res/hashnode/image/upload/v1628200125810/-oQjGDoYs.png)

## Setting up Jest and React Testing Library

We need to install a few dependencies to make Jest and RTL work properly with TypeScript.

```{bash}
$ yarn add -D @testing-library/dom @testing-library/jest-dom @testing-library/react @testing-library/user-event babel-jest jest jest-dom node-mocks-http ts-jest ts-loader
```

Specifically:
- All the `@testing-library` packages allow us to render our React components in what basically can be thought as a virtual browser and test their functionality
- `jest` is a testing framework, that we will use to write, run and structure our test suites
- `babel-jest` is used to transform and compile our code
- `ts-jest` and `ts-loader` allow us to test TypeScript-based code in Jest
- `node-mocks-http` will help us generate mocks of our request and response objects when testing our Next API routes

We need to create a file called `setupTests.js` at the root of our project, similar to what you would find in a `CRA`-generated app. This file will have a single line of code:

```{js}
import "@testing-library/jest-dom/extend-expect";
```
We also need to create a `.babelrc` file. It will contain the following:

```{json}
{
  "presets": ["next/babel"]
}
```
Let's now create the configuration file for `jest`, called `jest.config.js` at the root of our project:

```{js}
module.exports = {
	testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
	setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
	transform: {
		'^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest',
	},
};
```
As mentioned in the [`jest` docs](https://jestjs.io/docs/webpack#handling-static-assets), we don't usually need to use CSS files during tests, so we can mock them out the test suites by mapping every `.css` import to a mock file. To do this, let's create a new subfolder in our `styles` folder called `__mocks__`, and let's create a very simple file inside of it called `styleMock.js`, which will export an empty object:

```{js}
module.exports = {}
```

We have to let Jest know that whenever it encounters a css file import it should instead import this `styleMock.js` file. To do that, let's add another line to our `jest.config.js` file:

```{js}
module.exports = {
    {...},
	moduleNameMapper: {
		'\\.(css|less|scss|sass)$': '<rootDir>/styles/__mocks__/styleMock.js',
	},
}
```

We also need to make Jest aware of the path aliases that we defined in our `tsconfig.json` file. To our `moduleNameMapper` object, let's add two more lines:

```{js}
module.exports = {
    {...},
	moduleNameMapper: {
		'\\.(css|less|scss|sass)$': '<rootDir>/styles/__mocks__/styleMock.js',
		'^@pages/(.*)$': '<rootDir>/pages/$1',
		'^@components/(.*)$': '<rootDir>/components/$1',
	},
```

This will tell Jest that whenever it finds an import that starts with either `@pages` or `@component`, it should actually import from the `pages` and `components` folders in the root directory.

Let's test out our setup! I'll create a folder called, unsurprisingly, `tests` at the root of my project. I will mirror the organization of my project, meaning that I will have a `components` folder as well as a `pages` folder which also contains an `api` folder.

In the `pages` folder, let's write our first test for the newly created `index.tsx`, in a file called `index.text.tsx`:

```{ts}
import { render, screen } from '@testing-library/react';
import App from '@pages/index';

describe('App', () => {
	it('renders without crashing', () => {
		render(<App />);
		expect(
			screen.getByRole('heading', { name: 'Batteries Included Next.js' })
		).toBeInTheDocument();
	});
});

```

Let's start the test script by running `yarn test` and... We get an error.
```{sh}
  ● App › renders without crashing

    The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string.
    Consider using the "jsdom" test environment.
    
    ReferenceError: document is not defined
```

That `ReferenceError` is an issue that is caused by how Next.js renders its pages. Given that it tries to pre-render every page on the server side for better optimization and SEO, the `document` object is not defined, as it is a client-side only. According to [RTL's docs](https://testing-library.com/docs/react-testing-library/api/#render) about the `render` function:

> By default, React Testing Library will create a div and append that div to the `document.body` and this is where your React component will be rendered.

But we do not have a `document` to begin with, so this fails! Luckily, `jest` suggests a solution for this in the error message.

> `Consider using the "jsdom" test environment.`

Let's add a `jest-environment` string at the top of our `index.test.tsx`:

```{ts}
/**
 * @jest-environment jsdom
 */

import { render, screen } from '@testing-library/react';
import App from '@pages/index';

describe('App', () => {
	it('renders without crashing', () => {
		render(<App />);
		expect(
			screen.getByRole('heading', { name: 'Batteries Included Next.js' })
		).toBeInTheDocument();
	});
});
```

Now the tests will pass without any problem! Also, notice how we can import the `App` component from `'@pages/index` thanks to our `moduleNameMapper` work we did earlier.

Let's now test the API route. The default API example that `create-next-app` generates has a simple JSON response of `{"name": "John Doe"}`. In our `tests/pages/api` folder we can create a new file called `hello.test.ts` to mimic the `hello` API name:

```{js}
import { createMocks } from 'node-mocks-http';
import handler from '@pages/api/hello';

describe('/api/hello', () => {
	test('returns a message with the specified name', async () => {
		const { req, res } = createMocks({
			method: 'GET',
		});

		await handler(req, res);

		expect(res._getStatusCode()).toBe(200);
		expect(JSON.parse(res._getData())).toEqual(
			expect.objectContaining({
				name: 'John Doe',
			})
		);
	});
});
```

As you can see, we don't need to change the environment to `jsdom` as we are only using server-side code. In order to test our API route, we also need to mock the request and response that we pass to the API handler. In order to do this, we import the `createMocks` function from `node-mocks-http`, which helps us simulate a request and response object in a very intuitive manner and test it with Jest.

Let's run again `yarn test` and everything works just fine!

## Conclusion

There were a very large number of moving parts in putting together this template. A lot of the issues came with correctly choosing the `jest` related packages, as most were either incompatible with TypeScript or just weren't boding well with Next.js.

The template is available on my GitHub as [`Batteries-Included-Next.js`](https://github.com/antoniolofiego/Batteries-Included-Next.js). If this has helped you or you start using this template, let me know what you are working on as I would be extremely curious to know!

If you like this article, I'd suggest you to follow me on [Twitter](https://twitter.com/antonio_lofiego) and give a listen to my new podcast, [cloud, code, life|](https://twitter.com/cloudcodelife) where we chat about these technologies and more every week! Thanks for reading and good luck in your Next.js adventures!