A really simple example of TDD in JavaScript

A step by step introduction to Test Driven Development in JavaScript.

Note: There is an older version of this article in Java.

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 JavaScript or Java.


Timeline:

  • January 2018: First published in Java
  • May 2020: Rewrite in JavaScript

David

Thanks for visiting Learn it my way! I created this website so I could share my learning experiences as a self-taught software developer. Subscribe to for the latest content if this interests you!

Profile pic

David

Thanks for visiting Learn it my way! I created this website so I could share my learning experiences as a self-taught software developer. Subscribe to for the latest content if this interests you!