TypeScript unit tests best practices part 4: clean test suites structure

Thiago Oliveira Santos
5 min readJan 12, 2020

--

A proposal of clean structure for unit tests organization

Psst!

You can find part 1 here: Introduction,
And part 2 here: IDE and Project Setup,
And part 3 here: definitions and rules,
And part 5 here: how to “unit test” (almost) everything in TypeScript!

Hello again! What we’ll propose here is a simple guideline for easy tests organization!

Setup code

As you could see in part 2, we have set the test script to, first, run a file called setup.spec.ts. Here’s an example of a valid and simple setup code:

What we’re doing here?

  • We’re initializing sinon-chai library, which will help us to do some simple assertions;
  • We’re setting sinon to restore the test doubles state after each test. This is very important so one test doesn’t interfere with another;

What else we could do in this file?

  • We could initialize another chai libraries;
  • We could set another global behavior for the unit tests;
  • We could export some generic custom test functions to helps us in another tests, like a series of common assertions, for example;

What we shouldn’t do in this file?

  • We shouldn’t initialize an external service instance, like a database or a cache service. Unit tests are not supposed to access this kind of service!
  • We shouldn’t set objects that will be used in another test files. For the sake of simplicity and to avoid scope invasion, each test must create the minimal necessary objects for each test;
  • We shouldn’t stub any method! Leave the stubbing to each test suite!

Source code rules for a simple test structure

A good unit test begins with a good and testable source code. So, to achieve a homogeneous test structure, we recommend you to follow these rules for each source code file:

  • Each file must export only one object. It can be a const structure, function or class. If you want to have a file exporting multiple objects, create a folder, a file for each export and an index file to do it;
  • It should not have not exposed internal functions. Something like this makes a unit test for such functions impossible, requiring you to test it indirectly. Move them to another file or create a const structure to contains every exported function;
  • You should not have a source code with functions/classes and directly executable code, that is, code that will be ran along with the import of such file. It’s possible to test such scenarios but it’s harder and uglier. Actually, the only file with directly executable code that you should have in your project (an express API, for example) is the src/index.ts, the main one. A good example of main code is such as the follow:
import { start } from './server.ts';start();

This is a totally testable code. You can have additional operations in it, like the initialization of monitoring services, but it should be simple, without ifs or other complex statements. If you need some complex logic in it, put it in another file and import it.

Unit test structure explained

We propose, here, an homogeneous organization for each unit test, with the following rules:

  • The file must be in the test folder, in a directory mirroring the src source structure. Also, the file name must be theNameOfTheTestedFile.spec.ts. So, you’ll have an unit test for each file that contains testable lines of your project;
  • You must have a unit test for the main file. This will make all source files of your project to be considered when measuring coverage, as all file are directly or indirectly loaded by the main;
  • The first describe of your code must be the name of the only exported object;
  • If the exported object is a function, create a beforeEach to set all doubles that will be used in your test cases;
  • If the exported object is a class, create a beforeEach to set the instantiation of the target and, injected services as empty object;
  • Let the specific tests scenarios to be set in the correspondent it;
  • For classes tests, for each method, create a beforeEach to set all doubles that will be used in your test cases;
  • For each stub created, you need to have some assertion. That’s because stubs without assertions can be covering unexpected behaviors;
  • Assertions must be as restrictive as possible, as necessary;

Following this rules, you can have the test of a class like this:

The idea here is to maintain a rigid and simple organization so you can create test cases for all your source code and anyone can quickly identify what each part is doing.

But, let discuss a little deeper some details of the code above:

MyService is mocked as an empty object? Why’s that?

The is a great reasons for that!

  • You don’t access MyService at all, preventing scope invasion in your test;
  • Any line of MyClass that uses MyService will that calls a method you didn’t stub will throw an error, which will help you to don’t forget anyone;
  • as service is created in beforeEach, there is no risk of one test affecting another for some setup error;
  • Each test case will have only the necessary stubs code to run. No more, no less. This is great for long term maintenance, as this help the test to break when they’re suppose to. For example, what if the tested method starts to call another method of MyService? Don’t you agree that the test must be revisited? Well, as no stub was created for such method, you’ll have to;

Why do I have to assert each stub I declared?

Consider that, if the stub exists, maybe your test doesn’t throw and error when it is called, correct? Well, but what if the parameters informed for the stub are wrong? Then, maybe, your test as passing the false impression of assurance.

When you write unit tests, you must remember that you’re testing how an isolated part of your code is behaving, so, it is important to assert that all the external code are being accessed the way it is supposed to. The lack of assertion is the possibility of false assurance. So, even the assertion that guarantees that a method is not called is important.

How to test files without functions, classes or const structures?

To test a file like, for example, the main file proposed above, is very simple! As the code is loaded when the file is imported, you should import your file inside your test case. Let’s see how this works:

Look that, this is enough to guarantee the work of this unit. After all, what is the responsibility of our main file, in this case? Just to call server.start(), and that’s all!

Also, as we’re test our main file, every used source file are loaded, so we’ll not have any false coverage percentage!

What if I’m creating a lib and my the main file only do exports?

In this case you can just reference your main file in your setup.spec.ts, doing this:

import '../src/index';

This will be enough to guarantee all lines detection, but be aware! Does not do this if there is executable code outside functions or classes, as this code can be executed and can affect your tests! Preferentially, follow our advice and avoid this kind of code, leaving only the main one like this :)

I hope you enjoyed and until the next article!

--

--