Welcome back to our course on Test-Driven Development (TDD) in TypeScript using Jest
. In our previous lesson, we introduced the fundamentals of TDD and the Red-Green-Refactor workflow. Now, we will advance our TDD skills by focusing on generalizing solutions and enhancing the complexity of our testing scenarios.
As a brief reminder, TDD involves a repetitive cycle known as Red-Green-Refactor:
- Red: Write a failing test to clarify the new functionality you aim to implement.
- Green: Develop the smallest amount of code needed to make that test pass.
- Refactor: Clean up the code, enhancing its quality while maintaining its functionality and ensuring all tests remain passing.
In this lesson, we will expand upon the sum
function, demonstrating how to generalize it while following these TDD principles.
Before we dive into coding, let's review our current setup. You are already familiar with the sum
function from src/math.ts
and its corresponding test in test/math.test.ts
:
app/test/math.test.ts
TypeScript1import { sum } from '../src/math'; 2 3describe('sum function', () => { 4 it('should add two numbers correctly', () => { 5 // Act 6 const result = sum(2, 3); 7 8 // Assert 9 expect(result).toBe(5); 10 }); 11});
app/src/math.ts
TypeScript1export function sum(a: number, b: number): number { 2 return 5; 3}
This existing setup serves as a foundation. Now, we'll focus on expanding your understanding by generalizing the approach using TDD principles. Understanding where you've come from will help ensure future changes enhance our function without straying too far from the core logic.
To embrace the Red phase, let's introduce a new test case that will fail.
Update math.test.ts
to include more input scenarios:
app/test/math.test.ts
TypeScript1import { sum } from '../src/math'; 2 3describe('sum function', () => { 4 it('should add two numbers correctly', () => { 5 // Act 6 const result = sum(2, 3); 7 8 // Assert 9 expect(result).toBe(5); 10 }); 11 12 it('should add two more numbers correctly', () => { 13 // Act 14 const result = sum(5, 6); 15 16 // Assert 17 expect(result).toBe(11); 18 }); 19});
By including a new scenario with new inputs, this step is intentionally set to fail to define our target goal clearly.
Running this test:
Output:
1FAIL test/math.test.ts 2 sum function 3 ✓ should add two numbers correctly (3ms) 4 ✕ should add two more numbers correctly (4ms) 5 6 ● sum function › should add two more numbers correctly 7 8 expect(received).toBe(expected) // Object.is equality
This failure confirms that the new functionality needs addressing.
Now, let's transition to the Green phase, where our goal is to ensure all tests pass, including our new one. We can update our sum
function to use the generic response because it is the minimal solution.
app/src/math.ts
TypeScript1export function sum(a: number, b: number): number { 2 return a + b; 3}
Output:
1PASS test/math.test.ts 2 sum function 3 ✓ should add two numbers correctly (3ms) 4 ✓ should add two more numbers correctly (1ms)
Success! The green phase is complete, illustrating how effective writing minimal code to pass tests can be.
Finally, let's advance into the Refactor stage. The sum
function is very simple so there's nothing we can make better with the implementation. We do have a lot of duplication in our tests, however.
Let's introduce "parameterized testing". A parameterized test is a single test that has several inputs and outputs. It makes it easy to add a lot of test cases without duplicating too much test code.
app/test/math.test.ts
TypeScript1import { sum } from '../src/math'; 2 3describe('sum function', () => { 4 it.each([ 5 [2, 3, 5], 6 [5, 6, 11] 7 ])("adds two numbers correctly: sum(%s, %s) === %s", (a: number, b: number, expected: number) => { 8 // Act 9 const result = sum(a, b); 10 11 // Assert 12 expect(result).toBe(expected); 13 }); 14});
For now, there's no functional change — but strategically refactoring in this way offers benefits, maintaining clarity as complexity escalates over iterations.
In this lesson, we explored advancing our TDD approach by generalizing solutions and scaling scenarios:
- Red: Write a failing test to define new goals.
- Green: Confirm new goals via minimal passing code.
- Refactor: Support future needs by formalizing and improving code.
These principles remain central to creating robust, adaptable solutions in any software development process. Continue practicing these steps through additional tests and scenarios in your exercises. You're on the path to mastering evolving solution implementations and designing more comprehensive, generalized code with TDD. Keep applying these principles and practicing to cement your understanding further.