JavaScript and TypeScript course Β· Module 11: Testing with Jest
Testing TypeScript with Jest
In this lesson5
TypeScript will catch text passed instead of a number, but it will not catch a logic error: a function that returns a health of 110 instead of 100 is flawless as far as types go. Jest, in turn, does not understand TypeScript syntax on its own, so someone has to translate it. This lesson brings both worlds together.
TypeScript adds a type system to JavaScript, but tests still verify behavior at runtime. In Jurassic Park, TypeScript is like species documentation - it tells us what to expect, but tests verify whether the dinosaurs actually behave that way. In the testing pyramid, TypeScript belongs to the static analysis foundation, and tests work one floor up.
Configuring ts-jest
To test TypeScript with Jest, we need ts-jest. It is a transformer: before Jest runs a .ts file, ts-jest compiles it to JavaScript. The @types/jest package provides types for describe, it and expect, so that your editor and the compiler recognize them. I assume you already have jest and typescript in your project, and if not, add them to the same command:
1npm install --save-dev ts-jest @types/jestInstead of @types/jest, you can use the @jest/globals package and import describe, it and expect explicitly. @types/jest is maintained by the DefinitelyTyped community, which is why the Jest documentation advises keeping its version as close as possible to your Jest version.
Now you only need to tell Jest to pass files through ts-jest. A single preset line does that, and testMatch narrows the search to .test.ts files:
1// jest.config.js
2module.exports = {
3 preset: 'ts-jest',
4 testEnvironment: 'node',
5 testMatch: ['**/*.test.ts'],
6};You can also generate this file with the npx ts-jest config:init command. The testEnvironment: 'node' option means the tests run in plain Node.js, without a simulated browser. There is also a second route: babel-jest with the @babel/preset-typescript preset. Babel only strips the types without checking them, whereas ts-jest reports type errors by default and fails the test. That is why I recommend ts-jest when you care about that extra check.
Testing Interfaces and Types
The Dinosaur interface describes the shape of a specimen. The diet field accepts only three values, because it is a union of literal types. The Dinosaur['diet'] notation in the parameter takes the type of that one field, so the function will not accept a diet outside the list:
1// dinosaur.ts
2interface Dinosaur {
3 name: string;
4 species: string;
5 health: number;
6 diet: 'carnivore' | 'herbivore' | 'omnivore';
7}
8
9function createDinosaur(
10 name: string,
11 species: string,
12 diet: Dinosaur['diet']
13): Dinosaur {
14 return {
15 name,
16 species,
17 health: 100,
18 diet,
19 };
20}The test compares the whole returned object with toEqual, and separately checks that health is a number in the range from 0 to 100:
1// dinosaur.test.ts
2describe('createDinosaur', () => {
3 it('should return object matching Dinosaur interface', () => {
4 const dino = createDinosaur('Rex', 'T-Rex', 'carnivore');
5
6 // TypeScript checks types at compile-time
7 // Tests check values at runtime
8 expect(dino).toEqual({
9 name: 'Rex',
10 species: 'T-Rex',
11 health: 100,
12 diet: 'carnivore',
13 });
14 });
15
16 it('should have health as number', () => {
17 const dino = createDinosaur('Blue', 'Velociraptor', 'carnivore');
18 expect(typeof dino.health).toBe('number');
19 expect(dino.health).toBeGreaterThanOrEqual(0);
20 expect(dino.health).toBeLessThanOrEqual(100);
21 });
22});An interface cannot be tested directly: after compilation it disappears without a trace and does not exist at runtime. So we test values, and the compiler takes care of the types. The call createDinosaur('Pinky', 'Compsognathus', 'fruitarian') would not compile, and ts-jest would report an error before the test even started.
Testing Generic Functions
Repository<T> is an interface with a type parameter T: one repository can store dinosaurs, rangers or eggs, and TypeScript makes sure you do not mix species within one. The T extends { id: string } constraint requires every element to have an id field:
1// repository.ts
2interface Repository<T> {
3 items: T[];
4 add(item: T): void;
5 findById(id: string): T | undefined;
6 getAll(): T[];
7}
8
9function createRepository<T extends { id: string }>(): Repository<T> {
10 const items: T[] = [];
11
12 return {
13 items,
14 add(item: T) {
15 items.push(item);
16 },
17 findById(id: string) {
18 return items.find(item => item.id === id);
19 },
20 getAll() {
21 return [...items];
22 },
23 };
24}In the test we substitute the helper interface TestDino for T, and beforeEach creates a fresh repository before each test:
1// repository.test.ts
2interface TestDino {
3 id: string;
4 name: string;
5 dangerous: boolean;
6}
7
8describe('Repository<T>', () => {
9 let repo: Repository<TestDino>;
10
11 beforeEach(() => {
12 repo = createRepository<TestDino>();
13 });
14
15 it('should add and retrieve items', () => {
16 const dino: TestDino = {
17 id: '001',
18 name: 'Rex',
19 dangerous: true,
20 };
21
22 repo.add(dino);
23
24 expect(repo.getAll()).toHaveLength(1);
25 expect(repo.findById('001')).toEqual(dino);
26 });
27
28 it('should return undefined for missing item', () => {
29 expect(repo.findById('999')).toBeUndefined();
30 });
31
32 it('should return copy of items array', () => {
33 repo.add({ id: '001', name: 'Rex', dangerous: true });
34
35 const items = repo.getAll();
36 items.push({ id: '002', name: 'Blue', dangerous: true });
37
38 // Original array unchanged
39 expect(repo.getAll()).toHaveLength(1);
40 });
41});The last test guards an important promise: getAll returns a copy of the array ([...items]), so a push on the result does not change the repository. If someone returned items directly during a refactor, the alarm would go off. Also note that findById returns exactly the object we added, so even toBe would pass instead of toEqual.
Testing TypeScript Classes
The Enclosure class combines several TypeScript mechanisms. private hides the dinosaur array from code outside the class, and public readonly in the constructor is a shorthand that makes TypeScript create the read-only fields name, capacity and securityLevel for you. The addDinosaur method throws exceptions when the enclosure is full and when security is too weak for carnivores:
1// enclosure.ts
2class Enclosure {
3 private dinosaurs: Dinosaur[] = [];
4
5 constructor(
6 public readonly name: string,
7 public readonly capacity: number,
8 public readonly securityLevel: number
9 ) {}
10
11 addDinosaur(dino: Dinosaur): void {
12 if (this.dinosaurs.length >= this.capacity) {
13 throw new Error('Enclosure is full');
14 }
15 if (dino.diet === 'carnivore' && this.securityLevel < 8) {
16 throw new Error('Security level too low for carnivores');
17 }
18 this.dinosaurs.push(dino);
19 }
20
21 getDinosaurCount(): number {
22 return this.dinosaurs.length;
23 }
24
25 isEmpty(): boolean {
26 return this.dinosaurs.length === 0;
27 }
28}The tests use createDinosaur from the first example and a beforeEach that sets up a new enclosure for three specimens with security level 9 before each test:
1// enclosure.test.ts
2describe('Enclosure', () => {
3 let enclosure: Enclosure;
4
5 beforeEach(() => {
6 enclosure = new Enclosure('Zone A', 3, 9);
7 });
8
9 it('should create enclosure with properties', () => {
10 expect(enclosure.name).toBe('Zone A');
11 expect(enclosure.capacity).toBe(3);
12 expect(enclosure.securityLevel).toBe(9);
13 });
14
15 it('should start empty', () => {
16 expect(enclosure.isEmpty()).toBe(true);
17 expect(enclosure.getDinosaurCount()).toBe(0);
18 });
19
20 it('should add dinosaur', () => {
21 const dino = createDinosaur('Rex', 'T-Rex', 'carnivore');
22 enclosure.addDinosaur(dino);
23
24 expect(enclosure.getDinosaurCount()).toBe(1);
25 expect(enclosure.isEmpty()).toBe(false);
26 });
27
28 it('should throw when full', () => {
29 const dinos = [
30 createDinosaur('Rex', 'T-Rex', 'carnivore'),
31 createDinosaur('Blue', 'Velociraptor', 'carnivore'),
32 createDinosaur('Delta', 'Velociraptor', 'carnivore'),
33 ];
34
35 dinos.forEach(d => enclosure.addDinosaur(d));
36
37 const extra = createDinosaur('Echo', 'Velociraptor', 'carnivore');
38 expect(() => enclosure.addDinosaur(extra)).toThrow('Enclosure is full');
39 });
40
41 it('should reject carnivore with low security', () => {
42 const lowSecurity = new Enclosure('Zone B', 5, 5);
43 const rex = createDinosaur('Rex', 'T-Rex', 'carnivore');
44
45 expect(() => lowSecurity.addDinosaur(rex)).toThrow(
46 'Security level too low for carnivores'
47 );
48 });
49});The test does not reach into the private dinosaurs array, because the compiler would not allow it. It checks the state through the public methods getDinosaurCount and isEmpty, which is how the rest of the park uses the class. That is a good rule: test the behavior visible from the outside, not the implementation details.
Checking Object Shape (Asymmetric Matchers)
A word on naming: in TypeScript, a "type assertion" is the value as Type syntax, which only silences the compiler and checks nothing at runtime. Asymmetric matchers let you check that values have the expected shape, even when you do not know the exact values. getApiResponse is an example function returning an API response. expect.objectContaining requires only the listed fields, expect.any(String) accepts any string, and expect.arrayContaining an array that contains the given elements:
1test('response has correct shape', () => {
2 const response = getApiResponse();
3
4 // Checking object structure
5 expect(response).toEqual(
6 expect.objectContaining({
7 status: expect.any(String),
8 data: expect.any(Array),
9 timestamp: expect.any(Number),
10 })
11 );
12
13 // Checking array elements
14 expect(response.data).toEqual(
15 expect.arrayContaining([
16 expect.objectContaining({
17 id: expect.any(String),
18 name: expect.any(String),
19 })
20 ])
21 );
22});Such assertions are resilient to changing data: an identifier or a timestamp changes with every call, yet the test still passes as long as the shape matches. It is the runtime counterpart of an interface, checking in running code what TypeScript checked during compilation.
In TypeScript tests, describe, it and test work exactly as in JavaScript, and it is still just an alias of test. In the final project you will combine all the tools from this module, and in the lab below you will see a simulation of tests for the Enclosure class and the generic repository.
Remember: TypeScript is the species catalog and tests are the field observation - only together do they tell you that a dinosaur is what it claims to be.
Code for this lesson: index.js
1// Testing TypeScript with Jest
2console.log("=== Jurassic Park - TypeScript Testing ===\n");
3
4let passed = 0, failed = 0;
5function assert(cond, msg) {
6 if (cond) { passed++; console.log(" [PASS] " + msg); }
7 else { failed++; console.log(" [FAIL] " + msg); }
8}
9
10// --- Configuring ts-jest ---
11console.log("--- Configuring ts-jest ---\n");
12console.log("npm install --save-dev ts-jest @types/jest\n");
13console.log("// jest.config.js");
14console.log("module.exports = {");
15console.log(" preset: 'ts-jest',");
16console.log(" testEnvironment: 'node',");
17console.log("};\n");
18
19// --- Testing interfaces ---
20console.log("--- Testing TypeScript interfaces and classes ---\n");
21
22// TypeScript simulation (this is JS in Sandpack)
23function createDinosaur(name, species, diet) {
24 return { name, species, health: 100, diet };
25}
26
27class Enclosure {
28 constructor(name, capacity, securityLevel) {
29 this.name = name;
30 this.capacity = capacity;
31 this.securityLevel = securityLevel;
32 this.dinosaurs = [];
33 }
34
35 addDinosaur(dino) {
36 if (this.dinosaurs.length >= this.capacity) {
37 throw new Error("Enclosure is full");
38 }
39 if (dino.diet === "carnivore" && this.securityLevel < 8) {
40 throw new Error("Security level too low for carnivores");
41 }
42 this.dinosaurs.push(dino);
43 }
44
45 getDinosaurCount() { return this.dinosaurs.length; }
46 isEmpty() { return this.dinosaurs.length === 0; }
47}
48
49// Enclosure tests
50console.log("Suite: Enclosure\n");
51
52let enclosure = new Enclosure("Zone A", 3, 9);
53assert(enclosure.name === "Zone A", "has correct name");
54assert(enclosure.capacity === 3, "has correct capacity");
55assert(enclosure.isEmpty(), "starts empty");
56assert(enclosure.getDinosaurCount() === 0, "count is 0");
57
58const rex = createDinosaur("Rex", "T-Rex", "carnivore");
59enclosure.addDinosaur(rex);
60assert(!enclosure.isEmpty(), "not empty after add");
61assert(enclosure.getDinosaurCount() === 1, "count is 1");
62
63// Test: throw when full
64enclosure = new Enclosure("Zone B", 2, 9);
65enclosure.addDinosaur(createDinosaur("Rex1", "T-Rex", "carnivore"));
66enclosure.addDinosaur(createDinosaur("Rex2", "T-Rex", "carnivore"));
67try {
68 enclosure.addDinosaur(createDinosaur("Rex3", "T-Rex", "carnivore"));
69 assert(false, "should throw when full");
70} catch (e) {
71 assert(e.message === "Enclosure is full", "throws 'Enclosure is full'");
72}
73
74// Test: low security
75console.log("\nSuite: Security levels\n");
76const lowSec = new Enclosure("Zone C", 5, 5);
77try {
78 lowSec.addDinosaur(createDinosaur("Rex", "T-Rex", "carnivore"));
79 assert(false, "should reject carnivore");
80} catch (e) {
81 assert(e.message === "Security level too low for carnivores", "rejects low security");
82}
83
84// --- Generic Repository ---
85console.log("\nSuite: Generic Repository\n");
86
87function createRepository() {
88 const items = [];
89 return {
90 items,
91 add(item) { items.push(item); },
92 findById(id) { return items.find(i => i.id === id) || undefined; },
93 getAll() { return [...items]; },
94 };
95}
96
97const repo = createRepository();
98repo.add({ id: "001", name: "Rex", dangerous: true });
99
100assert(repo.getAll().length === 1, "has 1 item");
101assert(repo.findById("001").name === "Rex", "finds by id");
102assert(repo.findById("999") === undefined, "undefined for missing");
103
104// Test immutability
105const all = repo.getAll();
106all.push({ id: "002", name: "Blue", dangerous: true });
107assert(repo.getAll().length === 1, "getAll returns copy (immutable)");
108
109// --- expect.objectContaining ---
110console.log("\nSuite: Structural assertions\n");
111
112function getApiResponse() {
113 return {
114 status: "ok",
115 data: [{ id: "001", name: "Rex" }],
116 timestamp: Date.now(),
117 };
118}
119
120const response = getApiResponse();
121assert(typeof response.status === "string", "status is string");
122assert(Array.isArray(response.data), "data is array");
123assert(typeof response.timestamp === "number", "timestamp is number");
124assert(typeof response.data[0].id === "string", "item.id is string");
125assert(typeof response.data[0].name === "string", "item.name is string");
126
127console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What preset in Jest configuration is needed for testing TypeScript?
2. What is the difference between test() and it() in Jest?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Vertical ordering
Arrange the steps for configuring TypeScript tests with Jest:
- Horizontal ordering
Arrange the elements of a typed TypeScript test:
- Click in order
Arrange the elements of a test throwing an exception in a TypeScript class:
- Vertical ordering
Arrange the steps for testing a generic repository:
- Vertical ordering
Arrange the elements of jest.mock syntax with a module argument:
- Horizontal ordering
Arrange the elements of an assertion checking an object's shape:
- Click in order
Arrange the layers of the test pyramid from the base (most tests) to the top (fewest):
- Vertical ordering
Arrange the steps for mocking an external module in the correct order:
- Vertical ordering
Arrange the steps for writing a complete test from start to finish:
- Horizontal ordering
Arrange the elements of spyOn usage:
- Click in order
Arrange the steps for mocking a module in Jest:
- Vertical ordering
Arrange the steps for testing asynchronous code with fake timers: