Better unit tests with TypeScript

In my previous post I’ve talked a little about my experience with TypeScript and why I think you should give it a try. It was a very brief and overall summary of everyday work with TS. Now I would like to focus more on one specific area in which TypeScript is especially beneficial to me – unit tests.

There is a lot of developers surveys but I would like to draw your attention to the one conducted by JetBrains.

According to the results, only 59% of developers write unit tests. It means that a lot of apps we use is not tested and may be broken. So why is that? What’s the reason?

Well, actually, from a point of view of JavaScript developer, I could name a few:

  • we have to constantly switch between files with tests and a code. Because JS is dynamically typed language, tools we use to obtain accurate information about interface exposed by tested unit often simply doesn’t work.
  • usually tests are loosely coupled with the code. It is possible to completely rewrite interface of the function without breaking any existing test. Moreover, because there is no compilation stage, we don’t know about breaking tests until we run them.
  • in tests we often repeat some simple tasks so we tends to create helpers or use utility libraries. It decreases readability of tests and increases their  complexity.
    Imagine the test case in which your teammate used handcrafted “renderComponent” function to simplify rendering UI components in tests. Does it return mounted react component? Or some kind of representation (JSON based maybe? A text?)? With pure JS you can’t possibly know it, and you should not assume anything, you just have to check.

Those are only first three reasons I came up with. There is plenty of them, and that’s why writing unit test in JavaScript could be pretty unpleasant experience. But it doesn’t mean that we should abandon the idea of unit tests. It means that we need a better tool, a better language maybe? And for me and my teammates TypeScript is such a language.

So whether you are thinking about switching from JavaScript to TypeScript or you are coding in TypeScript right now but you are scared of unit tests, I would like to show you how TypeScript could help you mitigate difficulties mentioned above and how to get more benefits from tests written in statically typed language.

Side note: features mentioned below are not strictly related to the unit tests but they really shine in a process of writing tests – when you have to be focused on the code unit and its interface and not get distracted by searching for a missing information in entire project.

TL;DR;

  • Thanks to the static types and great IDE support you can quickly write new tests without constantly switching between code and test just to verify the API of tested component.
  • Unit tests written in TypeScript are tightly coupled to the code via static types and interfaces.
  • When you change the public interface of the component, you have to change the test, otherwise they won’t compile, so entire team have to care about tests and maintain them.
  • You can embrace useful techniques like “fakes”.
  • You can replace external libraries and tools by native TypeScript features which provide the same set of functionalities.

Benefits in every TypeScript project

Faster, safer

Typically, when you write unit tests, you create empty tests suite with some basic tests cases. Your tests are failing, and that’s ok. Now switch to the implementation and create some real code that is passing the tests:

Ok. I’ve skipped few “red light – green light” iterations in above example but you get the point. We have working code covered by unit tests. Easy, right?

But in non trivial examples there is much more code and much more test cases. You have to constantly switch between files with a code and a tests to check what kind of arguments are expected, what is a type of the result etc. This is bad for your focus and your workflow. You can invest in good IDE with sophisticated code autocompletion for JavaScript, but it is just a workaround and often simply doesn’t work well enough.

With TypeScript though, any decent code editor or IDE could help you with such issues. Let’s see how it looks like in VS Code (free editor):

See? No need to going to the source code, no more context switching. Information about shape of the API you exposed from imported module is right where you need it. It means that you can write unit tests much faster and you don’t have to guess or make assumptions about tested functionality.

Tightly coupled

Business requirements change, so the code changes as well. Moreover, sometimes we create bugs, so we should fix them. When we are changing the public API of our components, we should also change our tests, because they are outdated. But sometimes even when we forget about adjusting them to the new requirements they pass! Why? Because we were sloppy, we missed some things, ignored an edge case or didn’t cover some “if” statement. It happens, because after all, we are all just humans. But our tools should save us in similar situations, and the language itself should be the first one to notify us about desynchronization between code and tests.

Unfortunately JavaScript is not such language.

Consider the following scenario: we have the Validation class with one public method “result” which returns “true” or “false”:

We have tests for positive and negative outcome. Then we introduced a change to the constructor argument (not really in a spirit of open/closed principle), but tests don’t know about it. We can’t see any error indicator in the editor. We don’t have feedback until all of tests run (assuming that they are written to catch such changes).

In TypeScript test typically wouldn’t even compile in such case, because of mismatch of the returning types expected in a test and declared in a code. Our tests and code are now tightly coupled via types and interfaces. Test suite became integral part of our source code and building pipeline, so we can’t simply ignore them, it just won’t work. And we have that for free.

Invalid tests written in TypeScript won’t compile and run. You will notice errors even without investigating a test’s file.

Fakes

There is a technique related to unit testing called “fakes”. If you are familiar with unit tests you probably use stubs and mock instead of real dependencies, but there is a little problem with them – you depend on mocking mechanism, which sometimes is wired to the core platform features, like “require” function in node environment. It is a bit messy, to be honest. “Fakes” solve that problem. I won’t dive deep into the subject right now, because fakes will have their own post, but to put it in simple words: “fake” is an object implementing the same interface like your dependency, but with fake functionality. For example, you can have the class PassingValidator which always returns “true”, no matter what arguments you pass in, or FailingHttpClient which simulates network errors for every request. Because those “fakes” implement the same interfaces like real Validator or HttpClient, you can use them in your tests as a dependency, which is completely transparent for a tested unit of code.

Of course you can use fakes in vanilla JavaScript, in fact, your can pass to your code whatever argument you want and it could still run. In TypeScript however you must keep your fakes in sync with real entities, because they implement the same interface. It means, that in your test you always use up to date dependencies, and you can catch some bugs that would be swallowed in case of using mocks or stubs (because they could be used for pretty every type or interface).

Jest has built in mocking functionality, but it is very inconvenient in complex scenarios.
With fakes you can prepare simple objects with fixed behavior and use them as dependency for tested unit.

Readability

Naming is one of the hardest parts of programmer’s job. It’s really difficult to find good names for our objects and we often do it wrong, so our code sometimes tends to be barely understandable. Thankfully statically typed languages increase code readability. If we failed with creating clear name for a variable, at last we have described the type, so it should be easier to predict exact meaning of the author’s intent. The same rule applies to the tests.

"Invalid" (line 4) is not the best name in that case, but with type annotation it is pretty clear what that constant represents.

Furthermore, TypeScript is designed with a newest EcmaScript features in mind, and some of them were introduced to solve nagging problems of our community. Great example is “async/await” syntax, which is modern and more pleasant way to work with Promises.

You might remember “callbacks hell”. Similar issue is related to the Promises. If you don’t know what I mean, please look at the following snippet:

With “async/await” syntax that code looks a little bit cleaner:

Why am I telling that in context of unit testing? Because in unit tests we often have to prepare the state of the tested object. For example, we have the UserService, and we want it to fetch user data, try to authorize the user and return info whether authorization was successful or not. This is a common scenario – in order to test one functionality we need the object in some specified state and we have to use some API (often asynchronous) to trigger that state. Sometimes it looks messy and makes test unreadable.

Because unit tests should be like a documentation for a code, we should always strive for better readability and I believe that TypeScript with it’s modern syntax is great tool to achieve this.

Compare classic "promise based" approach in a first test and modern "async/await" syntax in a second test.

React and TypeScript

Strict props types checking

As we just saw, TypeScript is useful in unit tests, but you might say, that nowadays we all create our apps using one of the many frameworks, and they often come with their problems and own tooling integrated with popular IDE or editors. You are right. Frameworks have their specificity and it would be very hard to write an app in Vue or React without, for example, code highlighting or special eslint/tslint rules. Their authors know that and they create and maintain entire ecosystem of tools around theirs projects. Sometimes though, those tools mimic features available in statically typed languages.

If you are a React developer you probably heard of PropTypes. For those who didn’t – it is a simple library which validates the shape of a prop (props are for components like arguments are for functions). But if you need that functionality, why don’t you just switch to the TypeScript in which you have it for free?

Let’s see a very simple example of props checking with an interfaces:

Did you notice a typo in word "sucess"? Compilator did.

Now we have props validation implemented in language mechanism. No additional libraries are involved, just regular types and interfaces. Moreover, you can share and treat them like real entities in your code, not just like a fancy annotations for eslint.

And here you have one more example with more advanced technique called Higher Order Component:

A pretty simple Higher Order Component, which adds a specified prefix to the "title" prop of decorated component. Notice the interface in which a "title" property is declared as a string.
Now you can't decorate component without "title" property. There is a typo in line 8. Did you spot that?

Summary

We all should care about unit tests, because they provide the most actual documentation for a code. But to be useful, tests should always be up to date and tightly coupled with the code. TypeScript is just a great tool to achieve that, not by a tons of external libraries, but just by its nature.
Probably your code would become more verbose and a little longer with TypeScript, but at the end of a day it would be easier to read, understand and maintain by your teammates. And maintainability is our uttermost goal.

So if you still need an argument to introduce TypeScript in your company or project, I hope I’ve provided you a pretty solid one.

Author:
Kamil Zagrabski

Share:

Share on facebook
Share on twitter
Share on linkedin

Copyright © 2020. ecom sp. z o.o. All rights reserved.