This content originally appeared on DEV Community and was authored by Paul Everitt
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
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.
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
Your IDE probably has a pointy-clicky way to run this. For example in WebStorm and other IntelliJ IDEs:
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.
{
"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:
$ 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.
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:
- Jest as the test runner
- jsdom as a simulated browser
- react-testing-library for test helpers and an assertion library
Enough ceremony. Let’s run the tests:
$ 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:
Open src/app/App.tsx
and change save to reload
to save to reload!!
. You’ll see output the looks something like this:
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:
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:
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();
});
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");
});
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:
Let’s now write that label
function:
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:
test('generates a label', () => {
const result = label(42);
expect(result).toEqual("Hello REACT");
});
...the test will get angry:
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:
Now run the tests, but executing under the 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:
In fact, TypeScript was warning us about this:
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()}`;
}
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.
This content originally appeared on DEV Community and was authored by Paul Everitt
Paul Everitt | Sciencx (2021-05-11T20:04:21+00:00) React, TypeScript, and TDD. Retrieved from https://www.scien.cx/2021/05/11/react-typescript-and-tdd/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.