Rules of Testing
- First Rule: You can write no production code except to pass a failing test case
- Second Rule: You can only write enough of a test case to demonstrate a failure
- Third Rule: You can only write enough production code to pass the currently failing test case
FizzBuzz Game
- "Fizz" if a number is divisible by 3
- "Buzz" if a number is divisible by 5
- "FizzBuzz" if a number is divisible by 3 and 5
- Otherwise, show the number (let's show the number as a String)
Project Setup
- Open
Git Bash
- Run the following:
mkdir fizzbuzz && cd fizzbuzz
- Initialize a new node project:
npm init -y
- Install Parcel:
npm i parcel-bundler
- Install Jest and all dependencies:
npm i -D jest babel-jest @babel/core @babel/preset-env
- Make a
src
dir and atest
dir:mkdir src test
- Make a
js
dir in both:mkdir {src,test}/js
- Open in VSCode:
code .
Update package.json
update the scripts section of your package.json
file
{
...
"scripts": {
"test": "jest test/js"
},
...
}
Red
import FizzBuzz from "../../src/js/FizzBuzz"
describe("fizzbuzz", () => {
test("Should return 1", () => {
// Arrange
const underTest = new FizzBuzz();
// Act
const actual = underTest.parse(1);
// Assert
expect(actual).toBe(1);
});
});
First Failures
So all of our failures at this point are compilation failures. To fix these we need to make the FizzBuzz.js
file with the parse method.
All fixed!
So now we have the following object in src/js/FizzBuzz.js
:
class FizzBuzz {
parse(inputNumber) {}
}
export default FizzBuzz
Now run your tests to see this one fail. In GitBash run npm test
.
Red -> Green
Now, we do the SIMPLEST thing to make the test pass. In this case...
class FizzBuzz {
parse(inputNumber) {
return 1;
}
}
export default FizzBuzz
Remember, never write more code than you need to make the test pass. Even if you know how the code may change in the future. Be creative with your tests and let them dictate how your code should work.
Now, run your test to see it passing!
Green -> Refactor
Now that we have gotten to green in the red -> green -> refactor cycle, it's time to refactor! Remember, refactoring can take the form of properly naming things, deleting unnecessary code, removing duplication, or anything else that generally makes our code cleaner.
At this point we don't have any clean up to do though, so it's time to move back to red.
Refactor -> Red
Now that we've got a passing test for 1, let's write a test for 2.
...
test("Should return 2", () => {
// Arrange
const underTest = new FizzBuzz();
// Act
const actual = underTest.parse(2);
// Assert
expect(actual).toBe(2);
});
Run this test to see it fail.
Red -> Green
So let's change our class under test to make this test pass...
class FizzBuzz {
parse(inputNumber) {
if (inputNumber === 1) {
return 1;
}
return 2;
}
}
export default FizzBuzz
Remember, since return
is a break statement, we will never run the risk of returning two outcomes from the same method. Run your tests to see them pass.
Green -> Refactor
Time to make our code cleaner again. Now let's take a look at the logic of our method and think about how we can make it cleaner. At this point it's pretty clear that we are just returning the number we pass in. Let's make our method do that instead...
class FizzBuzz {
parse(inputNumber) {
return inputNumber;
}
}
export default FizzBuzz
Refactor -> Red
Time for our third test!
...
test("Should return Fizz", () => {
// Arrange
const underTest = new FizzBuzz();
// Act
const actual = underTest.parse(3);
// Assert
expect(actual).toBe("Fizz");
});
Run this to see the fail.
Red -> Green
Remember to do the simplest thing to make the test pass.
class FizzBuzz {
parse(inputNumber) {
if (inputNumber === 3) {
return "Fizz";
}
return inputNumber;
}
}
export default FizzBuzz
Green -> Red
Since there really isn't anything to refactor, it's time to turn red again! So, for our next test, we are going to choose 5.
...
test("Should return Buzz", () => {
// Arrange
const underTest = new FizzBuzz();
// Act
const actual = underTest.parse(5);
// Assert
expect(actual).toBe("Buzz");
});
Why 5 and not 4?
Red -> Green
class FizzBuzz {
parse(inputNumber) {
if (inputNumber === 5) {
return "Buzz";
}
if (inputNumber === 3) {
return "Fizz";
}
return inputNumber;
}
}
export default FizzBuzz
We now have a passing test for 5!
Green -> Red
Time to continue on our testing journey...
test("Should return Fizz", () => {
// Arrange
const underTest = new FizzBuzz();
// Act
const actual = underTest.parse(6);
// Assert
expect(actual).toBe("Fizz");
});
This gets us failing. Let continue on the simplest road to passing.
A Moment for Names
Wait...
Now we have two tests with the same description ('Should return Fizz'). We can use a nested describe block to help with that. This is what we should have now:
describe("fizzbuzz", () => {
...
describe("Should return Fizz", () => {
test("for 3", () => {
...
});
test("for 6", () => {
...
});
});
});
Now we can nest anything that should return us "Fizz" in here so we're organized.
Red -> Green
class FizzBuzz {
parse(inputNumber) {
if (inputNumber === 5) {
return "Buzz";
}
if (inputNumber === 3 || inputNumber === 6) {
return "Fizz";
}
return inputNumber;
}
}
export default FizzBuzz
Green -> Refactor
Now let's examine our parse
method and see how we can make this logic a little more clear
class FizzBuzz {
parse(inputNumber) {
if (inputNumber === 5) {
return "Buzz";
}
if (inputNumber % 3 === 0) {
return "Fizz";
}
return inputNumber;
}
}
export default FizzBuzz
So now we're using the modulus operator to see if the number we have is evenly divisible by 3. We do this by checking to see if zero is the remainder when we divide by 3.
Complete remaining test cases
Time to continue using this same logic to figure out the remaining numbers to test! Which numbers make sense to test? Which do we have enough confidence to skip at this point?