Testing — Trust Your Code
Break Things Before Users Do
Open interactive version (quiz + challenge)Real-world analogy
Testing is like a fire drill. You don't wait for a real fire to find out if the exit doors work! You practice (test) regularly so when something goes wrong, you KNOW everything works. Untested code is like a building that's never had a fire drill — scary! 🔥
What is it?
Testing means writing code that verifies your other code works correctly. Jest is the most popular testing framework for JavaScript/TypeScript, and NestJS has built-in testing utilities.
Real-world relevance
Companies like Google require tests for every code change. The saying goes: 'Code without tests is broken by design.' A good test suite lets you refactor fearlessly!
Key points
- Unit Tests — Unit tests verify individual functions or methods in complete isolation from the rest of your application. Mock all dependencies (database, APIs, other services) and test only the logic inside one function. If addTax(100) should return 110, write a test that asserts exactly that. Fast to run and easy to debug.
- Integration Tests — Integration tests verify how multiple parts of your system work together. For example, test that a controller correctly calls a service which correctly queries the database and returns the expected response. These tests catch bugs that unit tests miss — like incorrect wiring between components or wrong query results.
- E2E Tests — End-to-end tests simulate a real user interacting with your application from start to finish. Send actual HTTP requests to your running server and verify the complete response including status codes, headers, and body. In NestJS, use supertest to make requests against your app without a real server running.
- Test Coverage — Test coverage measures what percentage of your code is executed during tests. Tools like Istanbul (built into Jest) generate coverage reports showing which lines, functions, and branches are tested. Aim for 80%+ coverage on critical business logic. 100% coverage is rarely worth the effort — focus on important paths.
- Jest — Jest is the most popular testing framework for JavaScript and TypeScript. It includes a test runner, assertion library (expect), mocking utilities (jest.fn(), jest.mock()), snapshot testing, and code coverage reporting — all in one package. NestJS uses Jest by default and provides testing utilities that integrate seamlessly.
- Mocking — Mocking replaces real dependencies with controlled fakes during testing. Use jest.fn() to create mock functions that track calls, jest.mock() to replace entire modules, and jest.spyOn() to watch method calls. Mocks ensure your tests are fast (no real database), isolated (no network calls), and deterministic (same result every time).
- Test Organization — Structure tests with describe() blocks for grouping related tests and it() or test() for individual test cases. Use beforeEach() to set up fresh test data and afterEach() to clean up. Follow the Arrange-Act-Assert pattern: set up your data, execute the function, then verify the result. Keep each test focused on one behavior.
- Test-Driven Development (TDD) — TDD flips the workflow: write a failing test first, then write the minimum code to make it pass, then refactor. This Red-Green-Refactor cycle ensures every piece of code has a corresponding test from the start. TDD produces cleaner designs because you think about the interface before the implementation.
- Testing NestJS Services — NestJS provides a Test module that creates a lightweight application context for testing. Use Test.createTestingModule() to set up your service with mocked dependencies injected automatically. This lets you test service methods in isolation while leveraging the same dependency injection system your production code uses.
Code example
// 1. Unit test — test a function in isolation 🔬
describe('calculateDiscount', () => {
it('should apply 10% discount for orders over $100', () => {
expect(calculateDiscount(200)).toBe(180);
});
it('should not discount orders under $100', () => {
expect(calculateDiscount(50)).toBe(50);
});
it('should handle zero gracefully', () => {
expect(calculateDiscount(0)).toBe(0);
});
});
// 2. NestJS service test — with mocking! 🎭
describe('UserService', () => {
let service: UserService;
let prisma: PrismaService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UserService,
{
provide: PrismaService,
useValue: {
user: {
findMany: jest.fn().mockResolvedValue([
{ id: '1', name: 'Alex', email: 'alex@test.com' }
]),
},
},
},
],
}).compile();
service = module.get(UserService);
});
it('should return all users', async () => {
const users = await service.findAll();
expect(users).toHaveLength(1);
expect(users[0].name).toBe('Alex');
});
});
// 3. E2E test — test the whole API 🌐
describe('POST /auth/login', () => {
it('should return a JWT token', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'alex@test.com', password: 'pass123' })
.expect(200)
.expect(res => {
expect(res.body.access_token).toBeDefined();
});
});
});Line-by-line walkthrough
- 1. 1. Unit test — test a function in isolation 🔬
- 2. Grouping related tests together
- 3. Defining an individual test case
- 4. Asserting an expected result
- 5.
- 6.
- 7. Defining an individual test case
- 8. Asserting an expected result
- 9.
- 10.
- 11. Defining an individual test case
- 12. Asserting an expected result
- 13.
- 14.
- 15.
- 16. 2. NestJS service test — with mocking! 🎭
- 17. Grouping related tests together
- 18. Declaring a variable
- 19. Declaring a variable
- 20.
- 21. Setup that runs before each test
- 22. Declaring a variable
- 23.
- 24.
- 25. Opening block
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32. Closing block
- 33. Closing block
- 34. Closing block
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41. Defining an individual test case
- 42. Declaring a variable
- 43. Asserting an expected result
- 44. Asserting an expected result
- 45.
- 46.
- 47.
- 48. 3. E2E test — test the whole API 🌐
- 49. Grouping related tests together
- 50. Defining an individual test case
- 51. Returning a value
- 52. Method chaining on the previous expression
- 53. Method chaining on the previous expression
- 54. Method chaining on the previous expression
- 55. Method chaining on the previous expression
- 56. Asserting an expected result
- 57.
- 58.
- 59.
Spot the bug
describe('add', () => {
it('should add two numbers', () => {
expect(add(2, 3)).toBe("5");
});
});Need a hint?
Look at the expected value - is it the right type?
Show answer
The test expects the string '5' but add(2,3) returns the number 5. toBe uses strict equality (===), so 5 !== '5'. Fix: change toBe('5') to toBe(5).
Explain like I'm 5
Testing is like a fire drill at school. You don't wait for a real fire to check if exits work! You practice first. Testing your code is the same - you check everything works before real people use it. Better to find problems during practice!
Fun fact
The Mars Climate Orbiter crashed in 1999 because one team used metric units and another used imperial — a bug that testing would have caught. That $125 million mistake could have been prevented by a $5 test! 🚀💥
Hands-on challenge
Write a test for a function that converts Celsius to Fahrenheit. Test: 0°C = 32°F, 100°C = 212°F, -40°C = -40°F (yes, they're the same at -40!).
More resources
- Jest Documentation (Jest Official)
- NestJS Testing (NestJS Official)
- Testing in JavaScript (Fireship)