DEV Community

loading...
Cover image for React, TypeScript, and TDD

React, TypeScript, and TDD

pauleveritt profile image Paul Everitt ・8 min read

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. 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 and quite familiar to anybody who has used Create React App. 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:

$ npx create-react-app my-app --template typescript
Enter fullscreen mode Exit fullscreen mode

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:

Installing packages. This might take a couple of minutes.
Enter fullscreen mode Exit fullscreen mode

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:

$ npm start
Enter fullscreen mode Exit fullscreen mode

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

npm run start

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",
Enter fullscreen mode Exit fullscreen mode

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.

{
  "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"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

$ npm run-script build
Enter fullscreen mode Exit fullscreen mode

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.

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 three important choices:

Enough ceremony. Let’s run the tests:

$ npm run-script test
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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, 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 -- 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:

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();
});
Enter fullscreen mode Exit fullscreen mode

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

test('generates a label', () => {
  const result = label("React");
  expect(result).toEqual("Hello REACT");
});
Enter fullscreen mode Exit fullscreen mode

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

Let’s now write that label function:

export function label(name) {
    return `Hello ${name.toUpperCase()}`;
}
Enter fullscreen mode Exit fullscreen mode

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:

test('generates a label', () => {
  const result = label(42);
  expect(result).toEqual("Hello REACT");
});
Enter fullscreen mode Exit fullscreen mode

...the test will get angry:

Integer

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

Now run the tests, but executing under the debugger:

Run Under Debugger

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

In fact, TypeScript was warning us about this:

TypeScript Warning

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:

export function label(name: string) {
    return `Hello ${name.toUpperCase()}`;
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

Forem Open with the Forem app