React, TypeScript, and TDD
Tags: react, typescript, testing
Published: 2021-05-11 20:04:21

ReactJS is wildly popular and thus wildly supported. TypeScript is increasingly popular, and thus increasingly supported.

The two together? Getting a lot better. Those two, in the context of test-driven development, combined with smart tooling? It's hard to find accurate learning materials.

That three-part combination -- React, TypeScript, and TDD -- is the topic of this series. This article is a Part 1 summary of a 10-part [video/text/code tutorial on React, TypeScript, and TDD](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/project_setup/). In two later installments, we’ll recap later steps from the tutorial.

## Why Test-Driven Development?

Eat your vegetables!

Test-driven development, or TDD, is pitched as a way to do extra work up front, to improve quality and save time later on. Most people, when told that, hear: “Blah blah **extra work** blah blah blah” and take a pass.

This tutorial series tries to pitch test-first in a different light: it’s **faster** and more **joyful**.

Why is it **faster**? I’m writing a React component, and I want to see if it works. I leave my editor, go to my browser, click around in the universe, hope I didn’t break something in another route/view. With the style of development pitched in this article, you stay in your smart editor, in the few lines of test code, and watch as things gradually start working.

And don’t even get me started on debugging during component development, aka `console.log`. Instead, you sit in your test, running under NodeJS, and set breakpoints, as all the gods in the multiverse intended.

**Joyful**? *Testing*? 

That’s a big claim. But it’s true. Instead of breaking your mental “flow” going between tools and contexts, you stay in your IDE, where you have muscle memory atop muscle memory. Code on the left, test on the right, test output at the bottom.

Mess something up? You’ll *fail faster* with a broken test or even an IDE squiggly thanks to TypeScript. If you broke something that isn’t the URL being hot reloaded by create-react-app, you’ll know that too. It’s a feeling -- really, I’m not just saying this -- of calm, methodical progress.

Of course, you also get your vegetables into the bargain.

## Setup

I won’t belabor the details of getting started: it’s in the [tutorial step](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/project_setup/) and quite familiar to anybody who has used [Create React App](https://create-react-app.dev). Still, to get oriented, I’ll show a few things.

What is Create React App (CRA)? Modern React, like anything in frontend development, has gotten awfully fiddly. CRA is a scaffold to create new React projects, using a known set of working packages.

You could master the hundreds of npm packages and configuration yourself, *and* keep them up-to-date as things change. CRA not only generates a working project for you, it moves the ongoing configuration *into their package*. Meaning, they will keep it working. (Terms and conditions apply, consult a doctor before tinkering, offer not valid if you eject.)

Creating a new project using npx (the npm command to fetch and run a package) is easy:

```console
$ npx create-react-app my-app --template typescript
```

Modern IDEs probably automate this for you as part of the New Project wizard.

npx will then fetch the create-react-app package, run it, and pass the template argument saying to generate a package that uses TypeScript. You’ll probably get a laugh out of this self-aware log message:

```console
Installing packages. This might take a couple of minutes.
```

The command also initializes a git repo, creates a `package.json`, and does the equivalent of npm install for your generated package. At the time of this writing, the result is a mere 1,063 entries in the `node_modules` directory.

Thank you CRA for owning all that.

You now have a working Hello World in React and TypeScript. To see it in action, run:

```console
$ npm start
```

Your IDE probably has a pointy-clicky way to run this. For example in WebStorm and other IntelliJ IDEs:

![npm run start](https://www.jetbrains.com/webstorm/guide/static/5f4c67b9ebb6b64ee278d543c670414d/efc66/npm_tool_window.png)

You’ll see some log messages as the dev server starts, and a browser will open at `http://localhost:3000` -- convenient!

Where did “start” come from? Take a look at the “scripts” block in the generated package.json file:

```
"start": "react-scripts start",
```

It’s a shortcut to a console script provided by CRA.

But wait, there’s more! With the dev server still running, open `src/App.tsx` and some text in the `<p>`, then save. In a second or two, your browser shows the update. CRA is watching for changes, transparently executes the four trillion instructions to change the frontend code, and does a smart reload with the browser.

If you look at all of `package.json`, you’ll see that it is quite compact.

```json
{
  "name": "react_ts_tdd",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "@types/jest": "^26.0.15",
    "@types/node": "^12.0.0",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3",
    "typescript": "^4.1.2",
    "web-vitals": "^1.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
```


Well, “compact” relative to the amount of work it is doing.

The genius of create-react-app lies in moving a bunch of "What the hell is this?" configuration files, into its packages. Thus, they own those decisions and complexity. You can then upgrade those packages and gain new/fixed wiring of all the JavaScript build tools.

Let’s run one more of the scripts CRA provided:

```console
$ npm run-script build
```

This takes a while, as it hyper-optimizes a generated React site/app in the `build` directory. This can then be deployed to a server.

## Hello Test

“You got me excited about testing, no testing, where’s the testing!” You’re right! Let’s do some testing, following [the tutorial step that covers this](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/testing/).

First, some background. I know, I know, I’ll get to a test soon.

CRA is opinionated. It chooses important packages, generates the configuration, and keeps the setup working. For testing, [CRA has made](https://create-react-app.dev/docs/running-tests) three important choices:

- [Jest](https://jestjs.io/) as the test runner
- [jsdom](https://github.com/tmpvar/jsdom) as a simulated browser
- [react-testing-library](https://github.com/testing-library/react-testing-library) for test helpers and an assertion library

Enough ceremony. Let’s run the tests:

```console
$ npm run-script test
```

It’s running under the watcher, so it tells you it doesn’t have any tests that have changed, based on Git:

![Watched No Changes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mz7fi3e7o7u7k8d8hynk.png)

Open `src/app/App.tsx` and change `save to reload` to `save to reload!!`. You’ll see output the looks something like this:

![Watched Changed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xq8cr480qygsujtdl1ls.png)


The watcher has some options to limit what it looks for, which really helps productivity. This time, change “Learn React” in `src/App.tsx` to say “Master React”. The watcher re-runs the tests, which now *fail*:

![Watched Failed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z783fhv2g9y7vw9kmy6f.png)


In an IDE you might get a richer way to look at this. For example, in WebStorm, here’s what the failing test runner looks like:

![In An IDE](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rsce6vd5w73zz5fmtpfq.png)

What’s really happening here? What’s executing? As mentioned earlier, CRA uses Jest as a test running. That makes Jest a...wait for it...test *runner*. It provides configuration, command flags (such as the watcher), ways to find tests, etc. It also [bundles jsdom as the pre-configured test environment](https://jestjs.io/docs/configuration#testenvironment-string), which is a long way to say “browser.”

jsdom is really neat. It’s a fake browser, written in JS, that runs in NodeJS and pretends to render your markup and execute your JavaScript. It’s a super-fast, unobtrusive alternative to Chrome firing up for each test.

Jest also uses [testing-library](https://testing-library.com) -- specifically, its React integration -- for the format of the tests and the assertions where you check that the code works. 

What does that look like? What does an actual *test* look like? Here is the test that Create React App generates by default:

```typescript
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});
```

We’ll see more down below when we really get into TDD. But for now...this is a nice way to work: staying in your editor and failing faster.

## Debugging During Testing With NodeJS

We’ve already shown a lot, enough that -- at least for me -- is really compelling for working this “test-first” way. But there’s one more part that clearly beats the alternative: debugging. This is covered in the [text and video for the tutorial step](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/nodejs_debugging/) on this section. This section shows integration with a particular tool (WebStorm) but concepts apply elsewhere.

Imagine, instead of just an `<h1>` with a label, we wanted a function that calculated the “greeting”. This function might take an argument for the name to say hello to, and we want to uppercase that name.

We could write the function and insert the call in the heading. Let’s write a test first:

```typescript
test('generates a label', () => {
  const result = label("React");
  expect(result).toEqual("Hello REACT");
});
```

The test fails: we haven’t written a label function. In fact, our tool gave us a warning, saying we haven't even imported it:

![Missing Label](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/38qx7h8gz01kzt8xwsqn.png)

Let’s now write that `label` function:

```typescript
export function label(name) {
    return `Hello ${name.toUpperCase()}`;
}
```

Once we import it in `src/App.test.tsx`, the tests now pass again. That’s great, but if we pass it an integer instead of a string:

```typescript
test('generates a label', () => {
  const result = label(42);
  expect(result).toEqual("Hello REACT");
});
```

...the test will get angry:

![Integer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cq06rcysue612ve2e0yk.png)

Let’s say we can’t easily figure out the problem. Rather than sprinkling `console.log` everywhere, we can use...the debugger! Set a breakpoint on the line in the test:

![Breakpoint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x0p2jup0qvwo36pinz8t.png)

Now run the tests, but executing under the debugger:

![Run Under Debugger](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/opjjq6s2cn7b61vopkin.png)

Execution will stop on this line in the test. You can choose “Step Into” to jump into the label function and then poke around interactively. You then discover -- duh, integers don’t have a `toUpperCase` method:

![Stop At Breakpoint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5qiu3fk6nh95u7ln9ghx.png)

In fact, TypeScript was warning us about this:

![TypeScript Warning](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mzfhr5aty8o733q9z3iu.png)

As a way to help guard against this, and to “fail faster” in the future, add type information to the name argument for the `label` function:

```typescript
export function label(name: string) {
    return `Hello ${name.toUpperCase()}`;
}
```

Debugging during test writing -- and staying in NodeJS, thus in your tool -- is super-productive. It’s much more productive than `console.log` the universe, or using the browser debugger.

## Conclusion

Writing React components is usually an iterative process: write some code, switch to the browser, click around. When you have problems and need to poke around, it’s...complicated.

The combination of TypeScript, test-first, and smarter tooling gives an alternative. One where you “fail faster” and stay in the flow, code with confidence -- and dare I say, have more fun.

In this first part we set the scene. As the tutorial shows, we’ll get into real component development in the next two parts.