Imagine you’re in a small conference room, staring at a screen filled with code alongside three other developers. You’re mobbing, trying to complete a story card. Suddenly, you encounter a piece of code that no one fully understands. The whiteboard markers come out, theories fly, and everyone talks over each other, but nothing definitive emerges. Someone suggests checking the specification. Does anyone maintain a specification? If so, has someone updated it recently? Probably not.
What if a specification updated itself almost automatically as the code evolved? What if this specification used a format that developers could easily read and understand? The answer to these questions lies in Test-Driven Development (TDD).
What is Test-Driven Development?
If you’re unfamiliar, TDD is the practice of writing failing unit tests that represent the requirements before writing the code to make them pass. You iterate on this process—test, code, refactor—until the story card is complete. The unit tests become a lasting artifact of the project. They live on with the application and must pass every time developers update or merge the code. This means that as requirements change, developers must update the unit tests as well, ensuring they are always current.
These tests are what I like to call “The Living Specification.”
Let’s return to that small conference room and the mysterious code. After much confusion, I suggested to my coworkers: “Let’s read the unit tests. They should explain the code.” Sadly, the project had no unit tests. We spent another hour deciphering the code, wasting time and energy that could have been avoided.
This story illustrates the value of unit tests as a living specification. When written well, they provide a clear and accurate understanding of what the code is doing. But let’s see an example of how this concept applies.
Confusing Code Without Tests
Take the following code. At a glance, it seems to create an array of distinct AddressInfoChip objects from a list of people’s addresses. But nested operations and meaningless variable names reduce readability, making it hard to interpret.
public parseAddressInfo(): AddressInfoChip[] {
return this.noDupes(
this.people
.map((x) => x.address)
.filter((y) => y && Object.values(y).some((z) => !!z))
).map((a) => {
const b = {
value: a,
isSelected: false,
};
return b;
});
}
private noDupes(q: Address[]): Address[] {
const m = new Map<string, Address>();
return q.reduce((r, s) => {
const t = this.addressPipe.transform(s);
if (!m.has(t)) {
m.set(t.toUpperCase(), s);
r.push(s);
}
return r;
}, [] as Address[]);
}
This code may work, but without unit tests, there’s no safety net to ensure refactoring won’t break functionality. Let’s see how a robust suite of unit tests can serve as a living specification for this code.
Living Specification: Unit Tests
Here’s a list of unit tests that describe what the code should do:
- Return an empty list when there are no people.
- Return an empty list when no addresses are valid.
- Return a single address in a list when only one valid address is present.
- Return all unique addresses as a list.
- Include only valid addresses in the list.
From this list alone, you already have a good idea of what the parseAddressInfo
method is supposed to do. But let’s take it further and look at the actual tests.
Example Unit Tests
Here are the tests written for the code above:
describe('parseAddressInfo', () => {
let component: YourComponent;
let addressPipeMock: { transform: jest.Mock };
beforeEach(() => {
addressPipeMock = {
transform: jest.fn(),
};
component = new YourComponent(addressPipeMock as any);
});
it('should return an empty list of addresses when there are no people, () => {
component.people = [];
const result = component.parseAddressInfo();
expect(result).toEqual([]);
});
it('should return an empty list of addresses when no addresses are valid', () => {
component.people = [
{ address: null },
{ address: {} },
{ address: { street: '', city: '', state: '', zip: '' } },
] as any;
const result = component.parseAddressInfo();
expect(result).toEqual([]);
});
it('should return a single address in a list when only one valid address is present’, () => {
const address = { street: '123 Main St', city: 'Anytown', state: 'CA', zip: '12345' };
addressPipeMock.transform.mockReturnValueOnce('123 main st, anytown, ca, 12345');
component.people = [{ address }] as any;
const result = component.parseAddressInfo();
expect(result).toHaveLength(1);
expect(result[0]).toEqual({ value: address, isSelected: false });
});
it('should return all unique addresses as a list', () => {
const address1 = { street: '123 Main St', city: 'Anytown', state: 'CA', zip: '12345' };
const address2 = { street: '456 Elm St', city: 'Othertown', state: 'NY', zip: '67890' };
addressPipeMock.transform
.mockReturnValueOnce('123 main st, anytown, ca, 12345')
.mockReturnValueOnce('456 elm st, othertown, ny, 67890')
.mockReturnValueOnce('123 main st, anytown, ca, 12345');
component.people = [
{ address: address1 },
{ address: address2 },
{ address: address1 }
] as any;
const result = component.parseAddressInfo();
expect(result).toHaveLength(2);
expect(result[0]).toEqual({ value: address1, isSelected: false });
expect(result[1]).toEqual({ value: address2, isSelected: false });
});
it('should return a list of addresses that do not include invalid addresses and include valid ones', () => {
const validAddress = { street: '123 Main St', city: 'Anytown', state: 'CA', zip: '12345' };
const invalidAddress = { street: '', city: '', state: '', zip: '' };
addressPipeMock.transform.mockReturnValueOnce('123 main st, anytown, ca, 12345');
component.people = [
{ address: invalidAddress },
{ address: validAddress },
] as any;
const result = component.parseAddressInfo();
expect(result).toHaveLength(1);
expect(result[0]).toEqual({ value: validAddress, isSelected: false });
});
});
If that team in that small conference room could have just read these unit tests, they could have had effective communication, saved time and because of this saved budget.
parseAddressInfo
does actually have a bug that our team of developers is dealing with. Can you find it? If not take a minute and read the unit tests again, can you come up with a missing test case? What would you add to this living specification?
Upon review, there’s an edge case: case-insensitive duplicates. What if we named the test 'should evaluate the addresses as duplicates with case-insensitivity'
? Take a look at the test.
it('should evaluate the addresses as duplicates with case-insensitivity', () => {
const address1 = { street: '123 Main St', city: 'Anytown', state: 'CA', zip: '12345' };
const address2 = { street: '123 MAIN ST', city: 'Anytown', state: 'CA', zip: '12345' };
addressPipeMock.transform
.mockReturnValueOnce('123 main st, anytown, ca, 12345')
.mockReturnValueOnce('123 main st, anytown, ca, 12345');
component.parties = [
{ address: address1 },
{ address: address2 },
] as any;
const result = component.parseAddressInfo();
expect(result).toHaveLength(1);
expect(result[0].value).toEqual(address1);
});
Finally, this test will fail and we can fix the production to code to match the specification. To fix this, you simply remove the toUpperCase
from the value you set as the key in the Map
.
if (!m.has(t)) { m.set(t.toUpperCase(), s); r.push(s); }
Unit Tests as Living Documentation
Developers rely on unit tests as more than just a safety net — they serve as a living specification for your code. They provide clarity, document requirements, and ensure future changes don’t break functionality. In scenarios like the one in the conference room, they save time, money, and frustration. Writing and maintaining these tests as part of your development process isn’t just good practice; it’s essential for building robust, maintainable software.