Test Driven Development (TDD) is a development process that consists of the tests being designed and written before most of the code is written. Ideally, you create a test and then write just enough code to fulfill that test and then you spend time to refactor it and make it production quality code.
The goal of test driven development is to write code that works, is easily maintained, and ideally it’s really clean.
Why do Test Driven Development
Test Driven Development (TDD) should start with designing and developing tests for every small bit of functionality. This instructs us developers to only write / update code only if the automated tests are failing.
TDD has the potential keep the code orderly and support incremental and iterative development while also supporting continuous integration.
TDD is more than testing, its software development designed to enable successful refactor, continuous integration and eventually continuous deployment.
Benefits of Test Driven Development
TDD is a lot more than vanity metrics like the percentage of code covered by tests. Aiming for 50 – 75% is more than sufficient, but it shouldn’t be the goal. By practicing TDD it should be incredibly easy to hit 75% because you are testing code and conditions as you are reworking it.
There’s a considerable number of benefits for Test Driven Development.
- Reduces bugs when applied to legacy projects
- Decreases bug to code ratio in new projects
- Increases overall software quality
- Increases code quality
- Code refactoring is a lot less risky
- Continuous integration becomes less riskier to implement
- TDD is great for shortening the programming feedback loop – letting us developers know when there’s problems and where they are.
Here’s How to Perform TDD
- Add a test,
- Run all tests and see if the new test fails.
- Add code / update code
- Run all tests.
- Refactor & simplify
- Run All Tests.
- Repeat as required.
A proper TDD cycle should look like this:
- Write Test(s),
- Make it Run,
- Code,
- Repeat
Where can TDD Be used?
Test driven development can be used for new builds or for legacy systems. In my blog post, 5 Tips To Writing More Maintainable Code it’s one of my suggestions whenever approaching new projects or legacy systems. For legacy systems, you definitely want to start really small and add simple unit tests before doing any refactoring.
Avoiding tests causes test creation to eventually become painful and cause developers to avoid it at all costs. Writing great unit tests is done by following the code flow instead of worrying about vanity metrics like code coverage.
Test Driven Development Example
To create a really simple example of doing TDD with Javascript, I’m going to implement FizzBuzz. Fizzbuzz is a game used to teach children about division: basically if the number is divisible by 3 you say Fizz, if the number is divisible by 5 you say buzz and if the number is divisible by 15 you say Fizzbuzz.
Fizzbuzz is also a common way of testing whether a developer has a decent level of experience in a certain programming language or environment during job interview processes.
For JavaScript based projects (NodeJS, React, etc) I like to use Istanbul for Code Coverage and Mocha + Chai for my testing.
To add them to a new project I run the following commands
npm init
npm i --save-dev mocha
npm i --save-dev chai
npm i --save-dev nyc
Couple of rules I like to follow when practicing TDD:
- We should never write production code before we have a failing test.
- Each step should be small
- Commit as soon as the tests pass
- Refactor as needed and continue to run the tests.
So, if I follow these rules the first thing I should do is create a test class.
npm init
npm i --save-dev mocha
npm i --save-dev chai
npm i --save-dev nyc
First we start by creating the test class. In NodeJS and with mocha it should be located in a “test” folder and end with .test.js. All tests should contain asserts so that the tests aren’t pointless. 🙂
And finally, the completed test should look something like this:
/* global describe it */
const { expect } = require('chai')
const chai = require('chai')
const FizzBuzz = require('.././FizzBuzz')
/*
* Mocha has a lot of callbacks and hooks that are available like describe and it.
* Callbacks run in the order they are found in the file.
*/
describe('Testing Fizzbuz - one number at a time', () => {
it('should return a 1 when 1 is passed in', done => {
// expect comes from chai, it's the same as doing an assert. :)
expect(FizzBuzz(1)).to.equal(1)
done()
})
it('should return a 2 when 2 is passed in', done => {
expect(FizzBuzz(2)).to.equal(2)
done()
})
it('should return fizz when 3 is passed in', done => {
expect(FizzBuzz(3)).to.equal('fizz')
done()
})
it('should return fizz when 6 is passed in', done => {
expect(FizzBuzz(6)).to.equal('fizz')
done()
})
it('should return buzz when 5 is passed in', done => {
expect(FizzBuzz(5)).to.equal('buzz')
done()
})
it('should return buzz when 10 is passed in', done => {
expect(FizzBuzz(10)).to.equal('buzz')
done()
})
it('should return fizzbuzz when 15 is passed in', done => {
expect(FizzBuzz(15)).to.equal('fizzbuzz')
done()
})
it('should return fizzbuzz when 30 is passed in', done => {
expect(FizzBuzz(30)).to.equal('fizzbuzz')
done()
})
})
We need to create a very simple file so we don’t get stuck on the require failing, it will just return the number that’s passed in for now.
module.exports = function processNumber (x) {
return x
}
As you can see this is quite a few tests, which should be perfectly fine. If we run the tests a lot of them should fail because we haven’t implemented much yet.
Next we would implement one of the first conditions likely x % 3 returning fizz when it’s 0. So the processNumber function ends up looking like this:
module.exports = function processNumber (x) {
// using modulus because it's less code. :)
if (x % 3 === 0) {
return 'fizz'
} else {
return x
}
}
If we rerun the tests, more of them are passing. There’s still more to go, so we would keep working on this. Eventually, our final implementation should be really simple and look like this:
module.exports = function processNumber (x) {
// using modulus because it's less code. :)
if (x % 3 === 0 && x % 5 === 0) {
return 'fizzbuzz'
} else if (x % 3 === 0) {
return 'fizz'
} else if (x % 5 === 0) {
return 'buzz'
} else {
return x
}
}
I’ve uploaded a complete git repo to GitHub: https://github.com/brcline/tdd-example
Summarizing
- Test Driven Development is a process for modifying/creating code to have it pass tests. It’s a great approach for Refactoring without increasing more bugs.
- In Software Engineering it’s sometimes referred to as test first engineering or test first development.
- Quality software is easier to build when the tests get the same level of attention as the code does.
- Tests provide really good documentation on what the expected behaviour is.
Pingback: Running Serverless Framework Functions Locally - Brian Cline