Welcome to our next step in mastering Test Driven Development (TDD) in TypeScript, where we will focus on setting up a robust testing environment. As you might recall, the TDD process involves the Red-Green-Refactor cycle — starting with a failing test, writing the minimum code needed to pass it, and then refining the implementation. In this lesson, we will set up the tools necessary for testing with Jest
and ts-jest
, guiding you on how to create an efficient testing environment that complements the TDD cycle.
Jest
is a popular JavaScript testing framework that integrates smoothly with TypeScript through ts-jest
. While other options like Mocha and Chai are available, Jest
offers a more straightforward setup, which is why it's our choice for this course. Now, let’s dive into setting up our testing environment in a systematic way.
The first step in preparing our environment is installing the necessary libraries. We will use Yarn
as our package manager. Here’s how you can do it:
Bash1npm install -g yarn 2yarn add --dev typescript
Bash1yarn add --dev jest ts-jest ts-node @types/jest
jest
: The core testing framework.ts-jest
: A TypeScript preprocessor for Jest that makes it possible to test TypeScript code without compiling it to JavaScript first.ts-node
: Allows the execution of TypeScript code without pre-compilation.@types/jest
: Type definitions for Jest, ensuring proper TypeScript support.
Remember, on CodeSignal, these libraries are pre-installed, so you can focus on writing and running tests without worrying about setup.
To utilize ts-jest
with TypeScript, a configuration file is essential. Let’s create a jest.config.ts
file with the following content and place it in the project root:
TypeScript1import type {Config} from '@jest/types'; 2 3const config: Config.InitialOptions = { 4 transform: { 5 "^.+\\.tsx?$": "ts-jest", 6 }, 7}; 8export default config;
- The
transform
property tells Jest to usets-jest
for processing.ts
files, ensuring TypeScript compatibility. - This setup simplifies test writing and running, maintaining focus on the TDD process: Red-Green-Refactor.
Other configuration methods exist, but this one offers a streamlined approach tailored for our TypeScript requirements.
With the setup complete, running tests becomes straightforward. Use the following command to execute all tests:
Bash1yarn jest
For continuous feedback during development, use the watch mode:
Bash1yarn jest --watchAll
This mode automatically re-runs tests upon file changes, enhancing productivity and aligning with the TDD philosophy: quick feedback and iterative improvements.
With the environment ready, let's look at a test suite. We’ll utilize a User
class example:
The describe
block in Jest is used to group related tests together, enhancing the structure and readability of your test suite. By nesting describe
blocks, you can create a hierarchical organization that mirrors the structure of your code. This is especially useful for complex modules with multiple aspects to test.
TypeScript1describe('User', () => { 2 describe('Initialization', () => { 3 it('Creates Users correctly', () => { 4 // test logic 5 }); 6 }); 7 8 describe('User email tests', () => { 9 test('getEmail returns correct email', () => { 10 // test logic 11 }); 12 13 test('email contains @ symbol', () => { 14 // test logic 15 }); 16 }); 17});
In Jest, it
and test
functions are interchangeable and serve to define individual test cases. The choice between the two often boils down to personal preference or aligning with stylistic conventions:
it
: Tends to be used when writing tests in a behavior-driven development (BDD) style, where tests read more like sentences (e.g.,it('does something')
).test
: Preferred when using a more straightforward language, as it's a direct term matching the action of performing a test.
Ultimately, both achieve the same result. Select the one that best fits the readability and consistency standards of your codebase.
The beforeEach
function is used to run a specific block of code before each test in a describe
block. This ensures consistent setup, eliminating repetitive code and maintaining test independence.
TypeScript1beforeEach(() => { 2 user = new User('Jane Doe', 'jane@example.com'); 3});
Jest provides various matchers for assertions:
toBe
: Checks strict equality.toEqual
: Compares the equality of objects or arrays by checking their contents rather than reference or strict identity.toBeInstanceOf
: Verifies that an object is an instance of a class.toContain
: Ensures an item is in an array or a substring is present in a string.
TypeScript1expect(user.getName()).toBe('Jane Doe'); 2expect(user).toEqual({ name: 'Jane Doe', email: 'jane@example.com' }); 3expect(user).toBeInstanceOf(User); 4expect(user.getEmail()).toContain('@');
To handle asynchronous code, Jest uses async functions combined with await
. Tests can return promises or use the done
callback to indicate completion.
TypeScript1describe("fetchUser", () => { 2 it('fetches data asynchronously', async () => { 3 const user = await fetchUser(); 4 expect(user).toEqual({ 5 name: 'John Doe', 6 email: 'john@example.com', 7 }); 8 }); 9});
Jest’s .toThrow
matcher is used for testing code that should throw an exception. It can be combined with a specific error message or class.
TypeScript1test('invalid email throws an error', () => { 2 expect(() => { 3 new User('Invalid', 'invalid-email'); 4 }).toThrow('Invalid email'); 5});
TypeScript1import {User, fetchUser} from "../src/user"; 2 3describe('User', () => { 4 let user: User; 5 6 // Setup - runs before each test 7 beforeEach(() => { 8 user = new User('Jane Doe', 'jane@example.com'); 9 }); 10 11 it('Creates Users correctly', () => { 12 expect(user.getName()).toBe('Jane Doe'); 13 expect(user.getEmail()).toBe('jane@example.com'); 14 expect(user).toBeInstanceOf(User); 15 }); 16 17 describe('User email tests', () => { 18 test('getEmail returns correct email', () => { 19 expect(user.getEmail()).toBe('jane@example.com'); 20 }); 21 22 test('email contains @ symbol', () => { 23 expect(user.getEmail()).toContain('@'); 24 }); 25 }); 26 27 test('invalid email throws an error', () => { 28 expect(() => { 29 new User('Invalid', 'invalid-email'); 30 }).toThrow('Invalid email'); 31 }); 32 33 describe("fetchUser", () => { 34 it('fetches data asynchronously', async () => { 35 const user = await fetchUser(); 36 expect(user).toEqual({ 37 name: 'John Doe', 38 email: 'john@example.com', 39 }); 40 }); 41 }); 42});
In this lesson, we've successfully set up a TypeScript testing environment using Jest
and ts-jest
. Key accomplishments include:
- Installation and Configuration: Acquired the necessary libraries and created a streamlined Jest configuration to integrate smoothly with TypeScript.
- Execution: Learned how to run tests in both standard and watch modes to facilitate continuous testing.
We also explored various Jest patterns to enhance testing:
describe
for Grouping: Organizes tests hierarchically for better readability.it
vstest
: Both define test cases; choice depends on stylistic preference.beforeEach
: Runs common setup code before each test, promoting DRY principles.- Matchers like
toBe
,toBeInstanceOf
,toContain
: Used for assertions such as equality checks, instance verification, and substring presence. - Async Tests: Uses async/await for testing code involving promises.
- Exception Testing with
.toThrow
: Validates that expected exceptions are triggered.
With this groundwork in place, you're prepared to dive into practice exercises focusing on crafting tests with Jest. These exercises will deepen your understanding of Jest's features and improve your ability to write clear and effective tests. The upcoming unit will bring us back to TDD, building upon the skills you hone in these practical sessions.