farmdev

Safer Unit Testing in React with TypeScript

I hear a lot about how static typing makes JavaScript safer but what about its influence on code architecture? As someone who has worked extensively with dynamically typed languages, I was surprised to realize that type safe applications can be written in entirely new ways.

I'd like to illustrate a fast and effective unit testing strategy that is only possible once you have type safety. I'll be talking about React in TypeScript but this may apply to similar realms.

Automated tests are the cornerstone of development. I want my tests to run as fast as possible and I want each failure to guide me directly towards a fix. Unit testing is a great solution for this; the minimal code execution encourages speed and the isolation makes each failure informative. React already encourages composition and encapsulation so it's a great fit for unit testing.

But, wait, unit testing in a dynamic language is unsafe because there are no contracts between units. For example, if you mock a dependent React component using outdated props, how would you know? The unit test would still pass. Perhaps because of this, functional testing seems quite popular for React apps and I totally get it. The only type of test you can trust in a dynamic language is a functional test!

And then your application grows. It grows and grows and each functional test compounds under the weight of all the common building block components you are repeatedly rendering. You begin to get drowsy. You make some coffee. You check Twitter. What were you even working on?

Slow tests are a bummer. They zap my motivation to prototype new concepts for a large, established application and I want them dead.

It doesn't have to be this way.

Unit testing a React app is safe when you introduce static typing via TypeScript or Flow. The type safety frees you up to test components in isolation with a technique such as shallow rendering.

Shallow rendering is when you only render the topmost component in the tree of JSX returned by render(). You can still make assertions about nested components (like which props they get) but the nested render functions will not get executed. You can even use shallow rendering to test wrapped components, such as Redux connected components.

In a very elegant way (thanks to JSX), shallow rendering is a type of mock dependency injection but it's fully automatic, meaning you won't be swimming in mock configuration when reading test code. Using mocked dependencies results in less code to execute -- i.e. faster tests -- and it separates the concern of each test. A bug in a common building block component won't cause a cascade of failing tests. Your test failures will be concise and informative.

OK, so, what happens when you change the props accepted by a dependent component? The TypeScript compiler will instantly point to all JSX that sets outdated props. Nice! If you have a fancy editor, you may even be able to fix the JSX automatically.

Static analysis of component props is the key ingredient you can combine with shallow rendering to cook up a fast, effective, and highly scalable test suite. Remember how I said React encourages composition and encapsulation? This means you can trust that a component will do the right thing as long as it receives the correct props.

Again, I totally get the dangers of shallow rendering on its own without static anaylsis.

Wait, are we talking public static void main() level of typing? TypeScript does make JavaScript more verbose but it's not that bad. It infers a lot of types automatically and does duck typing. But, still, is it worth the overhead? This is a fair question and The TypeScript Tax makes a case that it's not worth it. The article doesn't dispute the benefits of static analysis but it claims you can achieve them just the same through linting.

I want to believe this but I'm not sure it's true. For example, I have seen React PropType linters get confused by ES6 destructuring. There are several variants of this bug still open today; the introspection abilities of a linter are limited. I'd be interested to read a follow-up article explaining how you can use linters to achieve parity with static typing. TypeScript is truly its own programming language so it stands a much better chance at introspection.

Beyond unit testing, static typing greatly improves development in JavaScript. Let's face it, native JavaScript allows you to make many programming mistakes without so much as a warning. For example, it doesn't enforce function arguments whatsoever and doesn't help you work with undefined, null, or false values (the source of many bugs). In TypeScript, it's a lot harder to make simple mistakes.

I had chosen static typing (with Flow) for a popular open source project where a key objective was to engage community participation. This may come as a surprise but the static typing helped new contributors approach the code. I saw firsthand how compiler errors guided those who were less experienced in JavaScript.

At the end of the day, TypeScript is still a bolt-on solution for statically typed JavaScript. The error messages are hard to read. I have fought with third party library definitions and it's not fun. I just want to build features, damnit.

The JavaScript ecosystem is an imperfect world with compromises and trade-offs. I would still choose to build my next large JavaScript application with static typing, probably using TypeScript.