Automated testing is an important aspect of software engineering. In the JavaScript ecosystem, TypeScript provides an extra layer of assurance with types in JavaScript. For testing, Jest has been the de facto testing framework for JavaScript until now. Both TypeScript and Jest have risen in popularity in the past 12 months. According to the 2022 State of JS survey, Jest is the most frequently used testing framework , with 68% of survey respondents using it and 93% aware of it in 2022.
This guide will show you how to use Jest to write effective unit tests for a TypeScript app.
What is TypeScript? TypeScript, according to the official website , is “JavaScript with syntax for types.” Developed by Microsoft, it’s a superset of JavaScript that is strongly typed, is object-oriented, and has better IDE support.
You don’t run TypeScript as such; the code is written in TypeScript (a .ts file), and the TypeScript compiler compiles it to JavaScript (a .js file). The compiled code is executed. Because of this compilation process, you can compile TypeScript to older versions of JavaScript if needed. In recent years, the popularity of TypeScript has gone up significantly. According to the 2022 State of JS survey, 28% of respondents write TypeScript 100% of the time, compared with only 11% writing JavaScript all the time. Another advantage of TypeScript is that it can be used in both frontend and backend (Node.js) applications.
Having types on top of a dynamic language like JavaScript makes the software safer in execution. With TypeScript, the support for types in an IDE gives software engineers insights into what’s available. For example, if you’re using a typed SDK for GitHub ’s API, you’ll know exactly what parameters to send in the request and the fields to expect in the response. You’ll also know about the required and optional parameters, as they will be defined in the types. This comes in very handy and is much better than combing through the documentation to find the obvious answers. In the next section, you’ll learn about the importance of automated testing.
Why is Automated Testing Important? Generally, any code that is written is tested; the point is when and how the output of the code is tested. The most basic way of testing any web application that can be accessed via a web browser is by hitting a URL and then verifying the output visually. Usually, software engineers write the code and reload their browser tab to see if the result is as expected. This is a manual form of testing.
Depending on the size of the project and the resources allocated to it, there might be a quality assurance (QA) engineer or department to do this type of testing before the software is released. Employing feature flags can help minimize the blast radius if anything goes wrong (feature flags will be a topic of discussion for another post).
The issue with manual testing is the long feedback loop. The engineer has to change the code, switch the context to a browser, hit refresh, and see if the code change worked. This is where automated unit testing comes in very handy. If the setup is done properly and the software engineer practices test-driven development (TDD), the engineer would write the test first and then the code. This is also called the Red Green Refactor cycle of TDD.
First, you as a software engineer write a test that will fail (red) because the code needed to do the work is not written; then you write the minimum amount of code to make the test pass (green). After that, you refactor the code and/or write the actual implementation without breaking your unit test.
Another thing to note here is that unit testing tests only one unit of code. This unit of code is usually a single function, so the tests are laser-focused, fast, and independent of external factors like network and file system. If the tests are written in watch mode, which will rerun the tests when the code changes, the feedback loop is measured in milliseconds. If the tests are run in an IDE, there is almost no context switching involved. This helps with both developer productivity and software quality.
The most important reason to write tests is to reduce the number of bugs reaching end users. There are multiple forms and layers of tests that help you to achieve this primary goal. Practices like TDD and unit testing also help create a great developer experience. Automated unit testing adds confidence that the code does what it is expected to do. It’s also useful when you’re changing existing code or adding new features, because running the whole test suite will catch any regression introduced.
To recap, automated unit testing is better due to the fast feedback loop, and it can be done repeatedly with consistent results. If you want higher-quality software, having only manual testing is not scalable. Use automated tests to cover most cases and manual testing to verify that the happy path works.
For JavaScript and TypeScript, among other testing frameworks, Jest is the most popular in terms of usage. In the next section, we’ll list some prerequisites for the sample app and testing the TypeScript application with Jest.
Prerequisites Before we dive in, below are some prerequisites for better understanding the code:
Prior experience with Node.js and Express.js in particular will be useful. Knowledge of using the NPM command line will be helpful to install packages It will be good if you have used TypeScript and know about its basics Any prior experience with GitHub will be beneficial. Now it is time to get your hands dirty with some code. Let’s get coding!
Example app For this guide on how to test a TypeScript application with Jest, you will write tests for a simple quotes API built with Express.js. It will serve up 10 quotes per page, and the final output will look like the example below:
It’s a simple API that sends back mock static data from an array. It does not connect to a database, since the main focus of this tutorial is to write tests. The application is structured as follows without the tests:
The TypeScript setup resembles the setup done in this guide . The bulk of the code is in the src folder, which has QuotesController and QuotesService in the respective controllers and services folders. There is a custom type called Quote with properties like id, quote, and author.
The entry point of the application is index.js. The app.js has an App class that instantiates Express and glues together with the controller and routes.
All the code is available on this GitHub repository for your reference. You can also view the working application deployed on Render .
The app is using TypeDI for dependency injection (which will be discussed later in this post for people who are new to it). It’s also using the NPM concurrently package to run the TypeScript compiler in watch mode and the server on watch mode with Nodemon, as seen in this package.json command . You can clone the repository with the no-tests-or-jest branch by running:
git clone -b no-tests-or-jest git@github.com:geshan/typescript-jest.git
You can go into the directory with cd typescript-jest and install the needed NPM modules with npm install. To run the project in the development node, you can run:
Then you can check the output by placing http://localhost:3000/api/quotes in your favorite browser’s address bar — this will result in:
In the next section, you’ll learn about dependency injection and how it’s related to unit testing.
The Relationship Between Unit Testing and Dependency Injection Unit testing is focused on the unit being tested, which is the function or the class. Anything that is outside of the function should be mocked for a unit test. This includes not only network calls and access to the file system, but also other code dependencies like another class or methods of a different class. This is where unit testing and dependency injection intersect.
Dependency injection is a pattern where any dependency used in a class is not instantiated within the class; it is injected from an external source into the class. This concept comes out of the inversion of control paradigm used to create loosely coupled software. Being able to inject any dependency into a class using a dependency injection container makes it very easy to send in the mock classes for unit testing.
In the example below, you will see how a mock Quotes Service class is injected and used while testing the Quotes controller.
This is why writing testable code is the backbone of writing useful tests.
Dependency injection is the bedrock for writing testable object-oriented code. For this example of Jest and TypeScript, a TypeDI library is used for dependency injection. You can learn more about TypeDI in this helpful tutorial . In the next section, you will install, configure, and use Jest to prepare for writing some unit tests.
Install Jest From a code point of view, you have cloned the no-tests-or-jest branch from the GitHub repository. There are no tests in the no-tests-or-jest branch, and Jest has not been installed. You are going to install Jest and configure it for testing with TypeScript.
To install Jest with TypeScript support, you can execute:
npm install -D jest @types/jest ts-jest
The above command installs Jest and types for Jest. Jest is the main library for testing and you are also adding related types for Jest. It also installs ts-jest that helps you test TypeScript projects with Jest by adding a transformer with source map support. If you’re interested, read more about ts-jest in their documentation .
You can configure the Jest options in the package.json file with a new key called jest, which looks like this:
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "./",
"testMatch": [
"/test/**/*.(spec|test).ts?(x)"
],
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"coverageReporters": [
"text",
"html"
],
"collectCoverageFrom": [
"**/src/**/**/*.{ts,tsx,js,jsx}",
"!app.ts",
"!index.ts",
"!dist/**"
],
"coverageDirectory": "/test/.coverage",
"testEnvironment": "node",
"setupFiles": ["/test/setup.ts"]
}
Jest has a lot of configuration options . The above configuration starts with moduleFileExtensions, which is an array of file extensions used in your app modules. For this demo application, the provided .ts, .js, and .json should suffice. Then the root dir is set to the root of the project, /, where the Jest config file and package.json are placed. After that, tests are expected to be in the /tests directory in the root folder with a .spec.ts or .test.ts suffix; the .tsx file extension is also supported.
Jest runs the code of your project as JavaScript, thereby the transforming process is needed to compile the TypeScript code to JavaScript using ts-jest. Subsequently, test coverage reports will be available in text and HTML. The coverage will be applied on the /src folder’s ts, tsx, js and jsx files excluding the app.ts and index.ts files and the dest folder. The code coverage will be placed in the /test/.coverage folder. Jest is expected to run on Node, and it will execute the /test/setup.ts once per test file.
You will also add the following command in the package.json’s scripts section:
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage"
There are three commands; the first one is the test command, which you run by executing npm t or npm test. This will run all the tests in your test suite with Jest. The next one is the watch command you can run with npm run test:watch. This command will run tests when the file changes. It will run tests only for the file that has been saved. You can press the A key to run all tests while in watch mode. To come out of watch mode, press Ctrl-C.
The last npm script in the list is test:cov for test coverage. It will run all the tests and generate the code coverage as per the above config values. Jest uses Istanbul JS in the background to report the code coverage. You’ll learn more about code coverage later in this tutorial. In the following section, you’ll see the code for the Quotes controller and write unit tests for that code.
The Quotes Controller and its Tests As a best practice for unit testing, you should always write tests for the code you have written. Therefore, you will write tests for the controller and service. Writing tests for instantiating Express and writing it up with the controller is not a blocker. Below is the code for the QuotesController:
import { Service } from 'typedi';
import { QuotesService } from '../services/QuotesService';
import { Quote } from '../types/Quote';
@Service()
export class QuotesController {
private quotesService: QuotesService;
public constructor(quotesService: QuotesService) {
this.quotesService = quotesService;
}
public getQuotes(page: number = 1): Quote[] {
return this.quotesService.getQuotes(page);
}
}
You start this class by importing Service from type di. This is used as a decorator to allow the QuotesController class to be injectable using the container. Next, you import the QuotesService and the Quote type. The quotes service will get the quotes from the data source, and each quote will be of the type Quote, which looks like:
export type Quote = {
id: number,
quote: string,
author: string,
};
It’s a simple type with an id that’s a number. It has two other attributes: quote and author, both of type string.
Going back to the controller, next, the Quotes controller class is defined with a Service decorator. For this decorator to work, you’ll need to set the experimentalDecorators and emitDecoratorMetadata to true in the tsconfig.json file.
After that, the constructor of the Quotes controller class is defined, which takes in the QuotesService as a dependency. Next, you define a method called getQuotes; this takes the page parameter, which is a number set to 1 by default. It has an array of type Quote as the return type of the method. In this method, you call the getQuotes method on the quotes service. You also pass in the page number for pagination. The Quotes service is responsible for getting the quotes data from the appropriate data source, which is a static array for this demo.
You can write the test for the QuotesController even without the QuotesService, as shown below:
import { QuotesController } from "../../../src/controllers/QuotesController";
import { QuotesService } from '../../../src/services/QuotesService';
import { Quote } from "../../../src/types/Quote";
describe('QuotesController', () => {
let controller: QuotesController;
const mockQuotesService = {
getQuotes: jest.fn()
} as QuotesService;
beforeEach(() => {
controller = new QuotesController(mockQuotesService);
});
it('should define quotes controller', () => {
expect(controller).toBeInstanceOf(QuotesController);
});
describe('getQuotes', () => {
it('should get quotes', () => {
mockQuotesService.getQuotes = jest.fn().mockReturnValueOnce([
{
id: 1,
quote: 'There are only two kinds of languages: the ones people complain about and the ones nobody uses.',
author: 'Bjarne Stroustrup'
},
{
id: 2,
quote: 'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
author: 'Martin Fowler'
},
] as Quote[]);
const quotes:Quote[] = controller.getQuotes();
expect(quotes).toHaveLength(2);
expect(quotes[0].author).toBe('Bjarne Stroustrup')
expect(quotes[1].quote).toEqual(expect.stringContaining('Any fool can write code that'));
expect(quotes[1]).toEqual(expect.objectContaining({
id: 2
}));
expect(quotes[1]).toEqual({
id: 2,
quote: 'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
author: 'Martin Fowler'
});
expect(mockQuotesService.getQuotes).toHaveBeenCalledTimes(1);
expect(mockQuotesService.getQuotes).toHaveBeenCalledWith(1);
});
});
});
The Jest unit test starts with importing the QuotesController, which is the System Under Test (SUT). You also import the QuotesService class, which is a dependency for the controller and the Quote type. Then the main describe is named the same as the class to be tested, which is QuotesController. Just below the describe, the controller variable of type QuotesController and the mockQuoteService are defined. The mock quote service has a getQuotes function which is assigned a Jest function.
In the beforeEach hook, the controller is assigned the QuotesController calls with the mock Quotes services passed into the constructor. As the controller variable will be reused in multiple tests and functions, it is defined in the beforeEach function on the outer scope. Then you encounter the first test: it simply tests that the controller variable is an instance of the QuotesController class.
After that, another describe block begins to test the getQuotes method. It has an it function that tests whether the controller method can return some quotes. Here, you set the Quote service’s getQuotes method to return a couple of quotes as an array of the “Quote” type. Then you call the getQuotes on the controller and expect it to have a length of 2. Next, you assert that the author of the first quote is Bjarne Stroustrup and that the quote attribute of the second quote is a string containing Any fool can write code that.
There are multiple variations of assertions included in this test, to give you an idea of another way to utilize the expectfunction in Jest. You can be lenient and expect that an object has an id and not care about the other attributes. On the other hand, you can be stringent and expect the whole object to be exactly equal to the passed value.
Toward the end of the test, you expect the mock service’s getQuotes to have been called exactly once and to have been called with the value 1.
If you run the test with npm t or npm test, it will pass and show the following output:
In the next section, you’ll witness the code for the Quotes service and tests for it.
Quotes Service and Tests for it The Quotes service is the layer responsible for getting the data by querying a data source. It also abstracts out the data source from the caller. For this guide, to keep the scope small, a static array with 17 quotes is used as the data. The Quotes service could have communicated with a relational database or a NoSQL database using an Object Relational Mapper (ORM), but that is out of the scope of a tutorial focused on testing. Below is the code for the Quotes service:
import { Service } from 'typedi';
import { Quote } from '../types/Quote';
@Service()
export class QuotesService {
public getQuotes(page: number): Quote[] {
if (page < 1) {
throw new Error('Page number should be 1 or more');
}
const quotes:Quote[] = [
{
id: 1,
quote: 'There are only two kinds of languages: the ones people complain about and the ones nobody uses.',
author: 'Bjarne Stroustrup',
},
{
id: 2,
quote:
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
author: 'Martin Fowler',
},
{
id: 3,
quote: 'First, solve the problem. Then, write the code.',
author: 'John Johnson',
},
{
id: 4,
quote: 'Java is to JavaScript what car is to Carpet.',
author: 'Chris Heilmann',
},
{
id: 5,
quote:
'Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.',
author: 'John Woods',
},
{
id: 6,
quote: "I'm not a great programmer; I'm just a good programmer with great habits.",
author: 'Kent Beck',
},
{
id: 7,
quote: 'Truth can only be found in one place: the code.',
author: 'Robert C. Martin',
},
{
id: 8,
quote:
'If you have to spend effort looking at a fragment of code and figuring out what it\'s doing, then you should extract it into a function and name the function after the "what".',
author: 'Martin Fowler',
},
{
id: 9,
quote:
'The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.',
author: 'Donald Knuth',
},
{
id: 10,
quote:
'SQL, Lisp, and Haskell are the only programming languages that I’ve seen where one spends more time thinking than typing.',
author: 'Philip Greenspun',
},
{
id: 11,
quote: 'Deleted code is debugged code.',
author: 'Jeff Sickel',
},
{
id: 12,
quote:
'There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies.',
author: 'C.A.R. Hoare',
},
{
id: 13,
quote: 'Simplicity is prerequisite for reliability.',
author: 'Edsger W. Dijkstra',
},
{
id: 14,
quote: 'There are only two hard things in Computer Science: cache invalidation and naming things.',
author: 'Phil Karlton',
},
{
id: 15,
quote:
'Measuring programming progress by lines of code is like measuring aircraft building progress by weight.',
author: 'Bill Gates',
},
{
id: 16,
quote: 'Controlling complexity is the essence of computer programming.',
author: 'Brian Kernighan',
},
{
id: 17,
quote: 'The only way to learn a new programming language is by writing programs in it.',
author: 'Dennis Ritchie',
},
];
const itemsPerPage:number = 10;
const end:number = page * itemsPerPage;
let start:number = 0;
if(page > 1) {
start = (page - 1) * itemsPerPage;
}
return quotes.slice(start , end);
}
}
The Quotes service starts by importing Service from typedi and the Quote type. It has no constructor, so it has no constructor dependencies. If a real data source were used, it could take in the Quotes repository as a parameter to get data from a data source like a relational database.
Next, you define the getQuotes function. It takes in the page parameter of the type number. Typing here is possible, as you are writing TypeScript. This page parameter is used to do basic pagination. First, the method checks whether the page parameter is a number less than 1, if that is the case, an error is thrown mentioning that page can be 1 or more. This method also has 17 quotes related to programming in the quotes array.
Toward the end of the method, there is the logic to do the pagination of 10 quotes per page. The logic is self-explanatory, and it uses the array slice function to get the pagination working. This means page 1 will give quotes from index 0-9, and page 2 will send back quotes with array index 10-16.
Let’s look at how you can write tests for the above QuotesService with full code coverage, the file is placed at `/test/unit/services/QuotesService.spec.ts’ with the following contents:
import { QuotesService } from "../../../src/services/QuotesService";
import { Quote } from "../../../src/types/Quote";
describe("QuotesService", () => {
let service: QuotesService;
beforeEach(() => {
service = new QuotesService();
});
it("should define quotes service", () => {
expect(service).toBeInstanceOf(QuotesService);
});
describe("getQuotes", () => {
it("should get mock fixed quotes", () => {
const quotes: Quote[] = service.getQuotes(1);
expect(quotes).toHaveLength(10);
expect(quotes[0]).toEqual({
author: "Bjarne Stroustrup",
id: 1,
quote:
"There are only two kinds of languages: the ones people complain about and the ones nobody uses.",
});
});
it("should get mock fixed quotes for page > 1", () => {
const quotes: Quote[] = service.getQuotes(2);
expect(quotes).toHaveLength(7);
});
it("should throw error for page number less than 0", () => {
expect(() => {
service.getQuotes(-1);
}).toThrow("Page number should be 1 or more");
});
});
});
This test file has four tests and three of them are for the getQuotes method of the QuotesService class. Similar to the above test for the controller, the Quote type is imported and the System Under Test (SUT) in this case is the Quotes service.
The first test checks that the quotesService variable is an instance of QuotesService. Then in the “describe” section for getQuotes, the first test verifies that if 1 is passed in as the page number, it returns 10 quotes and also matches the first quote to be an object with expected values.
The next test does the same with the page variable being passed as 2. It expects to get back 7 quotes that are relevant. The final test in this file handles the error-throwing scenario in which the page is passed in as less than 1. It passes the page number as -1 and expects it to throw an error with the correct message. With all these tests, the file has full code coverage; you can see it by running npm test, which is in the package.json file and gives the following output:
In the following section, you will learn about checking code coverage for the tests written.
Checking Code Coverage Code coverage is a metric that can trigger a healthy debate. For starters, having 100% code coverage does not mean bug-free code; rather, it means that the software engineer has made an effort to write tests that cover all the code. There can be logical errors or errors related to data that unveil edge cases that weren’t thought about when writing the code.
With Jest, to check the code coverage, you can execute jest --coverage; it has been included in the package.json scripts section as test:cov. This will translate to you running npm run test:cov to see the code coverage that looks like this:
As seen previously, you have written enough tests to cover all relevant files and all the code inside them. As the HTML reporter is also enabled in the config, you will see HTML files that have code coverage information at /test/.coverage/index.html. If you open the file, you will see something like what follows:
For instance, if you remove the test in the QuotesService.spec.ts file with the description should throw error for page number less than 0 seen in lines 31-34. Then run the coverage you will see:
This means line number 10 in the QuotesService is not covered by any tests, which is the result of removing the above-mentioned test. This is how code coverage works.
As a software engineering team, it’s wise to go after a high code coverage as agreed upon by the team. Obsessing over getting 100% code coverage doesn’t make for a healthy tech culture. As with most things, the amount of time and effort you spend to get the target code coverage should be logical, optimal, and justified.
Conclusion In this step-by-step tutorial, you learned how to add Jest to an existing TypeScript project. Then you wrote unit tests for two important files: the Quotes controller and the Quotes service.
In addition to learning the concepts of testing, you also saw practical examples like how code coverage works and got introduced to testing terminology within the context of unit tests.
Meticulous Meticulous is a tool for software engineers to catch visual regressions in web applications without writing or maintaining UI tests.
Inject the Meticulous snippet onto production or staging and dev environments. This snippet records user sessions by collecting clickstream and network data. When you post a pull request, Meticulous selects a subset of recorded sessions which are relevant and simulates these against the frontend of your application. Meticulous takes screenshots at key points and detects any visual differences. It posts those diffs in a comment for you to inspect in a few seconds. Meticulous automatically updates the baseline images after you merge your PR. This eliminates the setup and maintenance burden of UI testing.
Meticulous isolates the frontend code by mocking out all network calls, using the previously recorded network responses. This means Meticulous never causes side effects and you don’t need a staging environment.
Learn more here .