Testing in TypeScript
🧪 Testing in TypeScript
calculateDiscount() returns the right number. That's what tests are for. This chapter covers Jest with TypeScript, type-safe mocking, and how the DI container from the last chapter makes both unit and integration testing dramatically easier.
Types Aren't Tests
The type checker verifies shape — that you're passing a number where a number is expected. It says nothing about behavior:
Type Errors
Caught at compile time: wrong shape, wrong argument count, undefined not handled.
Logic Errors
Only caught at runtime, by tests: wrong formula, off-by-one, incorrect branching.
Setting Up Jest with TypeScript
The most common setup uses ts-jest so Jest compiles .ts files on the fly:
That generates a jest.config.js pointing Jest at the TypeScript preset. Test files follow the *.test.ts or *.spec.ts convention:
🎭 Type-Safe Mocking
Mocking Interfaces from Chapter 1
Because OrderService from the DI chapter depends on interfaces, not concrete classes, mocking is just building an object that satisfies the interface:
Typed Mocks with jest.Mocked<T>
For a mock that must track every method of a real implementation, jest.Mocked<T> gives you full autocomplete and type checking on the mock itself:
Hand-Rolled Mock
A plain object matching the interface + jest.fn() per method — no magic, fully typed by TypeScript's structural typing.
jest.Mocked<T>
Wraps an existing class's type so every mocked method keeps its original signature and return type.
Type-Safe Test Helpers: Object Builders
Constructing a full Order object by hand in every test is noisy. A typed builder with sensible defaults keeps tests focused on what actually varies:
A Typed Test Data Builder
Partial<Order> — the mapped type from Course 2 — is exactly what makes overrides optional on every field at once, without writing it out by hand.
🔗 Integration Testing
Unit tests isolate one piece with mocks. Integration tests wire real pieces together and check the seams — often using the DI container itself:
Unit Test vs Integration Test
Unit Test
One class, dependencies mocked. Fast, pinpoints exactly what broke.
Integration Test
Real container, real wiring, maybe an in-memory database. Slower, catches wiring mistakes.
💻 Coding Challenges
Challenge 1: Test a Pure Function
Write a formatCurrency(amount: number): string function and a Jest test suite covering zero, negative numbers, and large values.
Goal: Practice describe/it structure and multiple expect assertions per case.
Challenge 2: Mock an Interface Dependency
Take the OrderService from Chapter 1 and write a unit test that mocks both Database and Mailer, asserting each is called with the correct arguments.
Goal: Practice hand-rolled typed mocks without a mocking library beyond jest.fn().
Challenge 3: Build a Test Data Builder
Write a buildUser(overrides?: Partial<User>): User helper with sensible defaults, then use it in two different test cases that each override a different field.
Goal: Practice Partial<T> and the spread operator for flexible, type-safe test fixtures.
It's tempting to mock everything a class touches, but a test built entirely of mocks can pass even when the real integration is broken — every mock just returns whatever you told it to. Keep a handful of integration tests that use real (or in-memory) implementations through the actual container, so wiring mistakes get caught somewhere.
🎯 What's Next
With dependencies injectable and tests in place, the next chapter moves outward: API Design & Documentation — typing request/response shapes, REST conventions, and generating OpenAPI/Swagger docs straight from your TypeScript types.