Test-driven development (aka TDD) consists of 3 phases: the red, the green, and the refactor.
The transition from green to refactor phase involves the generalization of the code under test. Especially if you've been using the faking technique in the green phase.
The idea of triangulation is to use 2 assertions to drive a safer creation of the generic code.
Let's see in more detail how triangulation works.
Note: if you're not familiar with test-driven development, I recommend checking TDD in JavaScript video guide before continuing with the article.
1. Example: develop a sum calculator
A good way to understand the benefits of the triangulation technique is to follow an example.
Let's say that you'd like to create a simple function: calculate the sum of 2 numbers. And let's imagine that you're unsure whether the addition operator is the right way to implement the sum.
Step 1: red
When doing TDD, I always start with the simplest test possible. In this case, I'm going to import the sum function and check that it returns undefined
.
First, you need to write the unit test of the sum function:
import { sum } from './sum'describe('sum()', () => { it('should execute', () => { expect(sum()).toBeUndefined() })})
Of course, the test throws an error because the module sum.js
doesn't exist.
FAIL sum.spec.js ● Test suite failed to run Cannot find module './sum' from 'sum.test.js'
Step 2: green
Let's then create the sum.js
module exporting a simple function with an empty body:
export function sum() {}
The unit test passes and I'm successfully in the green phase.
PASS sum.spec.js sum() ✓ should execute (1 ms)
Step 3: red
Now let's continue with the proper addition testing. Let's update the unit test to verify whether the function returns correctly the sum of 2 numbers: 1
and 2
.
import { sum } from './sum'describe('sum()', () => { it('should calculate sum', () => { expect(sum(1, 2)).toBe(3) })})
Running the updated test triggers an assertion error because currently the sum()
implementation does nothing.
FAIL sum.spec.js sum() ✕ should calculate sum (3 ms) ● sum() › should calculate sum expect(received).toBe(expected) // Object.is equality Expected: 3 Received: undefined 3 | describe('sum()', () => { 4 | it('should calculate sum', () => { > 5 | expect(sum(1, 2)).toBe(3) | ^ 6 | }) 7 | }) 8 |
Step 4: green
Because the green phase has to be passed as soon as possible (with any programming sins you can imagine), let's use a fake implementation and simply return 3
.
export function sum() { return 3}
Now the fake function passes the unit test.
PASS sum.spec.js sum() ✓ should calculate sum (2 ms)
Step 5: red
In the previous 4 steps, I followed the standard TDD. Nothing fancy.
Now starts the interesting part.
Instead of going to the refactor phase to write the sum implementation, and because I'm unsure that 1 assertion is enough to test my future generic code, let's get back to the red phase and write another assertion:
import { sum } from './sum'describe('sum()', () => { it('should calculate sum', () => { expect(sum(1, 2)).toBe(3) expect(sum(3, 4)).toBe(7) })})
This is the triangulation technique in practice: you use 2 assertions to drive the generalization of the code.
Running the test fails because of the second assertion.
FAIL sum.spec.js sum() ✕ should calculate sum (4 ms) ● sum() › should calculate sum expect(received).toBe(expected) // Object.is equality Expected: 7 Received: 3 4 | it('should calculate sum', () => { 5 | expect(sum(1, 2)).toBe(3) > 6 | expect(sum(3, 4)).toBe(7) | ^ 7 | }) 8 | }) 9 |
Step 6: green
Having the 2 assertions that check the future code, let's write the proper implementation of the sum:
export function sum(n1, n2) { return n1 + n2}
The unit test successfully passes. The addition code has been generated from the 2 assertions and now I have more confidence in the correctness of my generic solution.
PASS sum.spec.js sum() ✓ should calculate sum (2 ms)
Step 7: refactor
Now the generic code is created and the assertions prove it working. You can remove one of the assertions:
import { sum } from './sum'describe('sum()', () => { it('should calculate sum', () => { expect(sum(1, 2)).toBe(3) })})
Of course, running the unit test still passes.
PASS sum.spec.js sum() ✓ should calculate sum (2 ms)
2. Triangulation
Having seen the triangulation in practice, let's formulate a simple definition of it.
Triangulation is a technique that involves writing 2 assertions to drive a safer creation of a more general implementation.
In the previous example, triangulation has been used in step 5 to force the creation of a more general solution in step 6.
Now you might be asking yourself: why exactly 2 assertions are necessary and why a single assertion is not enough?
3. Example: things going wrong
Let's say I take an alternative approach at step 5. Without using the triangulation technique I go directly to refactor phase and throw a flawed generic solution.
Step 5: refactor (alternative)
Let's try the following generic solution:
export function sum(n) { return n + 2}
What I've done is just dump a simple but flawed generic solution. What's interesting, is that the unit test, the one defined in step 3 with 1 assertion... still passes!
import { sum } from './sum'describe('sum()', () => { it('should calculate sum', () => { expect(sum(1, 2)).toBe(3) })})
PASS sum.spec.js sum() ✓ should calculate sum (2 ms)
But having used the triangulation technique here, the flawed generic solution wouldn't have passed the unit test with 2 assertions:
import { sum } from './sum'describe('sum()', () => { it('should calculate sum', () => { expect(sum(1, 2)).toBe(3) expect(sum(3, 4)).toBe(7) })})
FAIL sum.spec.js sum() ✕ should calculate sum (4 ms) ● sum() › should calculate sum expect(received).toBe(expected) // Object.is equality Expected: 7 Received: 5 4 | it('should calculate sum', () => { 5 | expect(sum(1, 2)).toBe(3) > 6 | expect(sum(3, 4)).toBe(7) | ^ 7 | }) 8 | }) 9 |
4. Conclusion
I like the triangulation technique because it helps to create a generic implementation by reducing misses along the way.
You will find the technique useful when you're not sure about the correctness of the generic code you want to write. Having 2 assertions can give you more confidence.
In what scenarios do you find the triangulation technique useful?