JavaScript and TypeScript course Β· Module 11: Testing with Jest
Why is Testing Important?
In this lesson4
Imagine Jurassic Park without a dinosaur DNA verification system. Every newly cloned species could have hidden genetic defects that would only reveal themselves when a tyrannosaurus breaks out of its enclosure. In the world of programming, testing plays exactly the same role - it is an early warning system that catches bugs before they reach users.
"We check every sample before anything hatches from an egg," repeats Dr. Rex, head of the lab. In this module you will build that kind of control system for code: a test suite that keeps watch on its own and checks whether the park's functions do what they promise.
What you will learn
- write tests in Jest and read the PASS/FAIL report
- pick matchers like sensors for a specific threat
- prepare and clean the enclosure before every test
- swap dangerous dependencies for safe mocks
- test asynchronous code and fast-forward time with a fake clock
- write tests before code with TDD and test TypeScript
Manual vs Automated Testing
Manual Testing
Manual testing is like personally inspecting every dinosaur in the park - time-consuming, prone to human error, and impossible to repeat in an identical way. See it in action with the calculateDinosaurAge function, which computes a specimen's age from its hatching year. We check it "by eye", printing the results to the console:
1// Manual testing - you have to check the result yourself
2function calculateDinosaurAge(birthYear) {
3 return 2024 - birthYear;
4}
5
6// Manual check in the console:
7console.log(calculateDinosaurAge(2020)); // Is this 4? I have to check myself...
8console.log(calculateDinosaurAge(2015)); // And this 9? Checking again myself...The code works, but the whole verification happens in your head: you have to remember that 2024 minus 2020 is 4. Nothing gets recorded, so after every change to the function the inspection has to start from scratch.
Automated Testing
Automated testing is like installing sensors in every enclosure - they work 24/7, immediately alert about problems, and always check the same conditions. In code, the test function gives a check its name, expect takes the actual result, and toBe compares it with the expected value - you will learn the details in the next lesson.
1// Automated testing - the computer checks for you
2function calculateDinosaurAge(birthYear) {
3 return 2024 - birthYear;
4}
5
6// Automated test:
7test('calculates dinosaur age', () => {
8 expect(calculateDinosaurAge(2020)).toBe(4);
9 expect(calculateDinosaurAge(2015)).toBe(9);
10 expect(calculateDinosaurAge(2024)).toBe(0);
11});
12// Result: PASS or FAIL - you know instantly!The expectations now live in the code, so the check takes milliseconds and gives the same answer every time. The function itself has not changed - only who keeps an eye on the result has. Watch out for a trap, though: the year 2024 is hardcoded both in the function and in the expectations, so the tests will still pass in 2030, when the function is already underestimating ages. A test checks only what you ask it to.
Types of Tests
The Testing Pyramid
In the world of testing, there is the concept of the testing pyramid. At its base are unit tests, in the middle are integration tests, and at the top are end-to-end (E2E) tests. The model was popularized by Mike Cohn in his book Succeeding with Agile. In the example below, getSpecies turns a short code into a full species name, getDinosaur and feedDinosaur are two cooperating park modules, and page is a browser driven by an E2E testing tool - the goto, click and fill methods come from Playwright.
1// 1. Unit Tests - test a single function
2// Fastest, cheapest, we need the most of them
3test('checks dinosaur species', () => {
4 expect(getSpecies('T-Rex')).toBe('Tyrannosaurus Rex');
5});
6
7// 2. Integration Tests - test module cooperation
8// Verify that modules communicate correctly with each other
9test('feeding system cooperates with dinosaur database', () => {
10 const dino = getDinosaur('Rex-001');
11 const result = feedDinosaur(dino, 'meat');
12 expect(result.status).toBe('fed');
13 expect(dino.hunger).toBe(0);
14});
15
16// 3. E2E Tests (End-to-End) - test the entire application
17// Slowest, most expensive, we need the fewest of them
18test('user can book a park visit', async () => {
19 await page.goto('/booking');
20 await page.click('#select-date');
21 await page.fill('#name', 'John Hammond');
22 await page.click('#submit');
23 expect(await page.textContent('.confirmation')).toContain('Booking confirmed');
24});All three tests end with an assertion, but they differ in scope. A unit test touches neither the database nor the network, so it takes a fraction of a millisecond and points straight at the guilty function. An E2E test launches a browser and the whole application - it is the closest thing to a real visitor, but when it fails, you only know that the problem lies somewhere on the route from the ticket office to the booking.
The Foundation and the Tip of the Pyramid
A foundation is often drawn under the pyramid: static analysis. A linter (e.g. ESLint) and TypeScript's type system check your code before you run any test at all, and they will catch a typo or text passed where a number was expected. At the tip, above the E2E tests, sits a small amount of manual testing, known as exploratory testing: a human hunts for strange behavior that nobody foresaw. So the full order from the base up is: static analysis, unit tests, integration tests, E2E tests and exploratory tests. Of the three classic levels, unit tests are the most numerous.
Test Proportions
How many tests of each kind? In 2015, the Google Testing Blog suggested this split as a good starting point:
- 70% unit tests - fast, precise, easy to write
- 20% integration tests - verify cooperation between modules
- 10% E2E tests - simulate a real user
It is a heuristic, not a law of nature - the authors themselves note that every team will pick its own numbers, as long as the pyramid shape is kept. An inverted pyramid, full of slow E2E tests and manual clicking, is a well-known anti-pattern called the ice cream cone.
Benefits of Testing
Testing is not a waste of time - it is an investment. Here are the main benefits:
- Detecting bugs early - the earlier you find a bug, the cheaper it is to fix
- Behavior documentation - tests describe how code should work
- Safe refactoring - you can change code knowing that tests will catch regressions
- Confidence in deployments - green tests mean production readiness
My advice: start with unit tests for small, pure functions such as calculateDinosaurAge. They are the cheapest to write and the quickest to repay you with caught bugs. In the very next lesson you will meet Jest, the tool that runs such tests.
The lab below runs a miniature test runner written in plain JavaScript. Look at the last test, marked "bug": it passes even though it guards a negative age. A green test is only as smart as the expectation you wrote into it.
Remember: a test is a sensor in the enclosure - once installed, it watches the dinosaur through every code change, day and night.
Code for this lesson: index.js
1// Testing JS/TS - Why is testing important?
2console.log("=== Jurassic Park - DNA Verification System ===\n");
3
4// Manual vs automated testing
5
6// EXAMPLE 1: Manual testing (problematic)
7function calculateDinosaurAge(birthYear) {
8 return 2024 - birthYear;
9}
10
11console.log("--- Manual testing ---");
12console.log("Age (2020):", calculateDinosaurAge(2020)); // I check it myself: is this 4?
13console.log("Age (2015):", calculateDinosaurAge(2015)); // I check it myself: is this 9?
14console.log("Age (2024):", calculateDinosaurAge(2024)); // I check it myself: is this 0?
15
16// EXAMPLE 2: Automated testing (reliable)
17function autoTest(description, actual, expected) {
18 const passed = actual === expected;
19 const icon = passed ? "PASS" : "FAIL";
20 console.log(`[${icon}] ${description}: got ${actual}, expected ${expected}`);
21 return passed;
22}
23
24console.log("\n--- Automated testing ---");
25autoTest("Age of a dinosaur hatched in 2020", calculateDinosaurAge(2020), 4);
26autoTest("Age of a dinosaur hatched in 2015", calculateDinosaurAge(2015), 9);
27autoTest("Age of a dinosaur hatched in 2024", calculateDinosaurAge(2024), 0);
28
29// EXAMPLE 3: Testing pyramid
30console.log("\n--- Testing pyramid ---");
31console.log("Base: Unit Tests (70%) - fast, precise");
32console.log("Middle: Integration (20%) - modules working together");
33console.log("Top: E2E Tests (10%) - the whole application");
34
35// Simulation of a simple test runner
36console.log("\n--- Mini Test Runner ---");
37
38function runTests(suiteName, tests) {
39 console.log(`\nSuite: ${suiteName}`);
40 let passed = 0;
41 let failed = 0;
42
43 tests.forEach(({ name, fn }) => {
44 try {
45 fn();
46 console.log(` [PASS] ${name}`);
47 passed++;
48 } catch (e) {
49 console.log(` [FAIL] ${name}: ${e.message}`);
50 failed++;
51 }
52 });
53
54 console.log(` Results: ${passed} passed, ${failed} failed`);
55}
56
57function assertEqual(actual, expected) {
58 if (actual !== expected) {
59 throw new Error(`Expected ${expected}, got ${actual}`);
60 }
61}
62
63runTests("DinosaurAge", [
64 {
65 name: "calculates age correctly",
66 fn: () => assertEqual(calculateDinosaurAge(2020), 4)
67 },
68 {
69 name: "handles current year",
70 fn: () => assertEqual(calculateDinosaurAge(2024), 0)
71 },
72 {
73 name: "handles negative age (bug!)",
74 fn: () => assertEqual(calculateDinosaurAge(2025), -1)
75 }
76]);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. Which layer of the test pyramid contains the most tests?