A step by step introduction to Test Driven Development in JavaScript.
Exercise
I am going to demonstrate TDD by completing FizzBuzz. I have chosen to show each step in JavaScript because most of my work so far has been in this language. However, the same concepts apply to every language (I am familiar with). The complete source code can be found on Github in JavaScript or Java.
The exercise is complete when the following input:
[1, 2, 3, 5, 6, 10, 15]
results in the following output:
'1, 2, Fizz, Buzz, Fizz, Buzz, FizzBuzz'
Things to keep in mind
When demonstrating this exercise I like to mention the following points:
- Don’t write any production code before you have a failing test
- Make each step as small and simple as possible.
Implementation
Here is the starter code for the test:
import fizzBuzz from './fizzBuzz'
describe('fizzBuzz', () => {
it('executes', () => {
expect(fizzBuzz()).toBe(undefined)
})
})
And here is the starter code for the implementation:
export default function fizzBuzz() {}
Make sure the test is green!
For those of you following along with the source code, you can run the tests in watch mode with npm test
.
Red, green, red, green, ..., green
The first real assertion can be written as follows:
describe('fizzBuzz', () => {
it('executes', () => {
expect(fizzBuzz([1])).toBe('1')
})
})
The following snippet will make the test pass:
export default function fizzBuzz() {
return '1'
}
How easy was that!
I then add another assertion to the test:
describe('fizzBuzz', () => {
it('executes', () => {
expect(fizzBuzz([1])).toBe('1')
expect(fizzBuzz([1, 2])).toBe('1, 2')
})
})
And fulfil it:
export default function fizzBuzz(input) {
return input.join(', ')
}
Here I implement Fizz when the entry is 3:
describe('fizzBuzz', () => {
it('executes', () => {
expect(fizzBuzz([1])).toBe('1')
expect(fizzBuzz([1, 2])).toBe('1, 2')
expect(fizzBuzz([1, 2, 3])).toBe('1, 2, Fizz')
})
})
export default function fizzBuzz(input) {
return input
.map((entry) => {
if (entry === 3) {
return 'Fizz'
}
return entry
})
.join(', ')
}
If you are not familiar with map
, you could use a for
loop instead:
export default function fizzBuzz(input) {
const result = []
for (const entry of input) {
if (entry === 3) {
result.push('Fizz')
} else {
result.push(entry)
}
}
return result.join(', ')
}
Then I implement Buzz when the entry is 5:
describe('fizzBuzz', () => {
it('executes', () => {
expect(fizzBuzz([1])).toBe('1')
expect(fizzBuzz([1, 2])).toBe('1, 2')
expect(fizzBuzz([1, 2, 3])).toBe('1, 2, Fizz')
expect(fizzBuzz([1, 2, 3, 5])).toBe('1, 2, Fizz, Buzz')
})
})
export default function fizzBuzz(input) {
return input
.map((entry) => {
if (entry === 3) {
return 'Fizz'
}
if (entry === 5) {
return 'Buzz'
}
return entry
})
.join(', ')
}
Here I implement Fizz if the entry is a multiple of 3:
describe('fizzBuzz', () => {
it('executes', () => {
expect(fizzBuzz([1])).toBe('1')
expect(fizzBuzz([1, 2])).toBe('1, 2')
expect(fizzBuzz([1, 2, 3])).toBe('1, 2, Fizz')
expect(fizzBuzz([1, 2, 3, 5])).toBe('1, 2, Fizz, Buzz')
expect(fizzBuzz([1, 2, 3, 5, 6])).toBe('1, 2, Fizz, Buzz, Fizz')
})
})
export default function fizzBuzz(input) {
return input
.map((entry) => {
if (entry % 3 === 0) {
return 'Fizz'
}
if (entry === 5) {
return 'Buzz'
}
return entry
})
.join(', ')
}
The same for Buzz if the entry is a multiple of 5:
describe('fizzBuzz', () => {
it('executes', () => {
expect(fizzBuzz([1])).toBe('1')
expect(fizzBuzz([1, 2])).toBe('1, 2')
expect(fizzBuzz([1, 2, 3])).toBe('1, 2, Fizz')
expect(fizzBuzz([1, 2, 3, 5])).toBe('1, 2, Fizz, Buzz')
expect(fizzBuzz([1, 2, 3, 5, 6, 10])).toBe(
'1, 2, Fizz, Buzz, Fizz, Buzz'
)
})
})
export default function fizzBuzz(input) {
return input
.map((entry) => {
if (entry % 3 === 0) {
return 'Fizz'
}
if (entry % 5 === 0) {
return 'Buzz'
}
return entry
})
.join(', ')
}
Here I implement FizzBuzz when the entry is multiple of 3 and a multiple of 5:
describe('fizzBuzz', () => {
it('executes', () => {
expect(fizzBuzz([1])).toBe('1')
expect(fizzBuzz([1, 2])).toBe('1, 2')
expect(fizzBuzz([1, 2, 3])).toBe('1, 2, Fizz')
expect(fizzBuzz([1, 2, 3, 5])).toBe('1, 2, Fizz, Buzz')
expect(fizzBuzz([1, 2, 3, 5, 6, 10])).toBe(
'1, 2, Fizz, Buzz, Fizz, Buzz'
)
expect(fizzBuzz([1, 2, 3, 5, 6, 10, 15])).toBe(
'1, 2, Fizz, Buzz, Fizz, Buzz, FizzBuzz'
)
})
})
export default function fizzBuzz(input) {
return input
.map((entry) => {
if (entry % 3 === 0 && entry % 5 === 0) {
return 'FizzBuzz'
}
if (entry % 3 === 0) {
return 'Fizz'
}
if (entry % 5 === 0) {
return 'Buzz'
}
return entry
})
.join(', ')
}
This might be a good time to commit the code. Make sure there are no lint warnings/errors and the test is green beforehand! You can run npm run precommit
if you are following along with the source code.
Refactor, green, refactor, ..., green
First I remove some duplication:
export default function fizzBuzz(input) {
return input
.map((entry) => {
const multipleOf3 = entry % 3 === 0
const multipleOf5 = entry % 5 === 0
if (multipleOf3 && multipleOf5) {
return 'FizzBuzz'
}
if (multipleOf3) {
return 'Fizz'
}
if (multipleOf5) {
return 'Buzz'
}
return entry
})
.join(', ')
}
Make sure the test is still green!
Finally, I decide to extract processEntry
into a separate function:
function processEntry(entry) {
const multipleOf3 = entry % 3 === 0
const multipleOf5 = entry % 5 === 0
if (multipleOf3 && multipleOf5) {
return 'FizzBuzz'
}
if (multipleOf3) {
return 'Fizz'
}
if (multipleOf5) {
return 'Buzz'
}
return entry
}
export default function fizzBuzz(input) {
return input.map(processEntry).join(', ')
}
At this point, I tend to prefer to amend the previous commit with git commit --amend
. Make sure there are no lint warnings/errors and the test is green beforehand (with npm run precommit
)!
Final Thoughts
That's the end of the exercise. I hope you enjoyed it and were able to learn something new. The most important take-away from this exercise is to take small steps! The complete source code can be found on Github in Java or JavaScript.
Before you go… Thank you for reading this far! If you enjoyed the article, please don't forget to ❤️ it.
I write about my professional and educational experiences as a self-taught software developer, so click the +FOLLOW button if this interests you! You can also check out my website or subscribe to my newsletter for more content.
You might also like:
- Learn JavaScript with these resources
- Testing Apollo Server with Typescript
- Learning material - software development (starting with Intro to CS)
Top comments (0)