TDD
TDD
Methodology that uses
a test-first approach to help guide the design
of a solution
TDD Mantra
- Nunca escribas código para producción sin tener primero un test unitario que falle
- Escribe la menor cantidad de código posible para hacer que el test pase - recuerda YAGNI
- Cuando el test pase, busca oportunidades para mejorar el código, vuelve a correr los test al terminar
- Agrega un nuevo test para la siguiente funcionalidad de menor complejidad que agregue valor a tu programa
Don't write any production code without writing
first a unit test
You might find that you code already handles the functionality
First write the unit test
for the simplest case
of your functionality
If you begin with a unit test that handles all the functionality it'll probably
be more clutter than necessary
Then write a unit test
for the next simplest
case that adds value
to your functionality
The idea is to "find" the solution adding small increments of complexity.
This usually leads to cleaner code.
When writing production code, write just
enough code to make
the unit pass - YAGNI
Again, you want to have a clean solution so building it little by little is usually
the best way to achieve this.
TDD Cycle
- Red: you write a unit test that doesn't pass
- Green: you write the simplest code to pass the test
-
Blue: refactor code, both on the
production code and the test code
Red
Is ok if a test doesn't
pass because there's
a compile error, it's a consequence of using the test to drive the design
Green
Write the simplest code, don't feel temped to
make the code clean
or sophisticated
Blue
Refactor; Test Code is
first citizen so apply all
the best practices on it.
Blue
Once you have a unit test for a functionality; making it clean and "sophisticated" is way easier
Step by Step example
Bolos
- Se juegan 10 turnos ("frames") por jugador
- En un turno, el jugador tiene 2 lances ("rolls") para tratar de tumbar 10 bolos ("pins")
- Si un jugador tumba los 10 bolos en el primer intento, se llama una moñona ("strike") y el puntaje de ese turno es 10 más los 2 siguientes lanzamientos
- Si un un jugador tumba los 10 bolos usando los 2 intentos, se llama una media moñona ("spare") y el puntaje de ese turno es 10 más el siguiente lanzamiento
- Si un jugador no tumba los 10 bolos entonces el puntaje de su turno es igual al número de bolos que tumbó
Bowling Game
How do you code a
solution to find the score
of a finalized game?
What would be
the simplest functionality?
A game where player doesn't knock
any pins
import org.junit.*;
public class GameTest {
@Test
public void gutterGame() {
}
}
Design Decisions
- How do we create an object for this class?
- What methods and what signature should will support the expected functionality?
Since Unit Tests are
client code, it will help
you know if your
API is easy to use
Since your API will be testable it also usually means it's clean
import org.junit.*;
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;
public class GameTest {
@Test
public void gutterGame() {
Game game = new Game();
for (int times = 1; times <= 20; ++times) {
game.roll(0);
}
assertThat(game.score(), is(0));
}
}
Hamcrest is a library
that helps you write
cleaner tests. Is available in a lot of languages.
What's the best unit test?
A method that is really simple to understand and doubles as documentation
Avoid lots of lines.
Avoid conditionals.
Avoid loops.
If you can't avoid, then make them really simple!
First Test
Red
Ok, test doesn't pass
since there are
compilation errors,
let's fix them
public class Game {
public void roll(int pins) {
}
public int score() {
return 0;
}
}
First Test
Green
public class Game {
public void roll(int pins) {
}
public int score() {
return 0;
}
}
Is this code stupid?
It's not!
It's just super specific.
It only works for
the "gutter" game
It's good that's super specific, we'll make it more general step by step until it's general. This helps produce clean code.
How do we know it's "stupid"?
Write a new test that fails.
If we can't write one, then
is probably not as "stupid"
as we first thought
First Test
Blue
Nothing to refactor
Our first test was great! Since the production code is stupid to write, it let
us focus on other
important things:
object-creation and API
Next simplest functionality to add value to our code will be to enable accumulation of code
@Test
public void allOnes() {
Game game = new Game();
for (int times = 1; times <= 20; ++times) {
game.roll(1);
}
assertThat(game.score(), is(20));
}
Second Test
Red
Red
Expected: is <20>
but: was <0>
at GameTest.allOnes(GameTest.java:25)
Always check the error message of your unit test.
If not very useful,
improve it!
This test makes it clear that our design is flawed, because we need to accumulate the pins!
public class Game {
private int score = 0;
public void roll(int pins) {
score += pins;
}
public int score() {
return score;
}
}
Second Test
Green
"It's stupid, where are we handling the bonuses for Strikes and Spares"?
Nowhere, because we haven't write a unit test that needs to do that!
We'll do soon...
Second Test
Blue
Production Code looks
ok, but, Test Code! ...
public class GameTest {
@Test
public void gutterGame() {
Game game = new Game(); // repetition
// repetition and code at a lower
// level of abstraction
for (int times = 1; times <= 20; ++times) {
game.roll(0);
}
assertThat(game.score(), is(0));
}
@Test
public void allOnes() {
Game game = new Game(); // ditto
// ditto
for (int times = 1; times <= 20; ++times) {
game.roll(1);
}
assertThat(game.score(), is(20));
}
}
JUnit let us handle the repeated object-creation, for the loop we'll add a level of abstraction
public class GameTest {
private Game game;
@Before
public void init() {
game = new Game();
}
@Test
public void gutterGame() {
rollMany(20, 0);
assertThat(game.score(), is(0));
}
@Test
public void allOnes() {
rollMany(20, 1);
assertThat(game.score(), is(19));
}
private void rollMany(int times, int pins) {
for (int n = 1; n <= times; ++n) {
game.roll(pins);
}
}
}
Private methods on test code are great! Remember, test code is First Citizen!
We know we aren't calculating bonuses yet,
so let's try that!
Simplest functionality
will be for a Spare
@Test
public void oneSpare() {
game.roll(5);
game.roll(5); // spare
game.roll(3);
rollMany(17, 0);
assertThat(game.score(), is(16));
}
Third Test
Red
This test really challenges our design! It seems we need a major overhaul
It's clear we need to
store rolls to calculate
the bonuses, because
it'll be a big change,
let's do it slowly
First let's forget bonuses
for a moment
//@Test
public void oneSpare() {
game.roll(5);
game.roll(5); // spare
game.roll(3);
rollMany(17, 0);
assertThat(game.score(), is(16));
}
Now do the
simplest refactor
public class Game {
private int[] rolls = new int[21];
private int currentRoll = 0;
public void roll(int pins) {
rolls[currentRoll] = pins;
++currentRoll;
}
public int score() {
int score = 0;
for (int i = 0; i < rolls.length; ++i) {
score += rolls[i];
}
return score;
}
}
This passes all of our previous tests! What about the newest?
@Test
public void oneSpare() {
game.roll(5);
game.roll(5); // spare
game.roll(3);
rollMany(17, 0);
assertThat(game.score(), is(16));
}
Every test but the last passes, that's good!
The issue with our code
is that we our mixing implementation details
(an array) with
domain details (frames)
public class Game {
private int[] rolls = new int[21];
private int currentRoll = 0;
public void roll(int pins) {
rolls[currentRoll] = pins;
++currentRoll;
}
public int score() {
int score = 0;
for (int i = 0; i < rolls.length; ++i) {
score += rolls[i];
}
return score;
}
}
becomes
public class Game {
private int[] rolls = new int[21];
private int currentRoll = 0;
public void roll(int pins) {
rolls[currentRoll] = pins;
++currentRoll;
}
public int score() {
int score = 0;
int frameIndex = 0;
for (int frame = 1; frame < 10; ++frame) {
if (rolls[frameIndex] + rolls[frameIndex + 1] == 10) {
score += 10 + rolls[frameIndex + 2];
} else {
score += rolls[frameIndex] + rolls[frameIndex + 1];
}
frameIndex += 2;
}
return score;
}
}
Third Test
Green
Maybe you don't agree with the implementation and you think you can come with something better, you are welcome to try!
Third Test
Blue
There's a lot to refactor!
public class Game {
private int[] rolls = new int[21];
private int currentRoll = 0;
public void roll(int pins) {
rolls[currentRoll] = pins;
++currentRoll;
}
public int score() {
int score = 0;
int frameIndex = 0;
for (int frame = 1; frame < 10; ++frame) {
if (rolls[frameIndex] + rolls[frameIndex + 1] == 10) {
score += 10 + rolls[frameIndex + 2];
} else {
score += rolls[frameIndex] + rolls[frameIndex + 1];
}
frameIndex += 2;
}
return score;
}
}
score() mixes levels of abstractions, I don't need to know there's an array!
public int score() { // look ma, is there's an array?!
int score = 0;
int frameIndex = 0;
for (int frame = 1; frame <= 10; ++frame) {
if (isSpare(frameIndex)) {
score += spareBonus(frameIndex);
} else {
score += frameScore(frameIndex);
}
frameIndex += 2;
}
return score;
}
private int spareBonus(int frameIndex) {
return 10 + rolls[frameIndex + 2];
}
private boolean isSpare(int frameIndex) {
return rolls[frameIndex] + rolls[frameIndex + 1] == 10;
}
Now test code...
@Test
public void oneSpare() {
game.roll(5);
game.roll(5); // spare
game.roll(3);
rollMany(17, 0);
assertThat(game.score(), is(16));
}
There's that
hideous comment!
@Test
public void oneSpare() {
rollSpare();
game.roll(3);
rollMany(17, 0);
assertThat(game.score(), is(16));
}
private void rollSpare() {
game.roll(5);
game.roll(5);
}
Next functionality?
One Strike!
@Test
public void oneStrike() {
game.roll(10); // strike
game.roll(3);
game.roll(4);
rollMany(17, 0);
assertThat(game.score(), is(24));
}
Forth Test
Red
By now, production
almost writes itself!
public int score() {
int score = 0;
int frameIndex = 0;
for (int frame = 1; frame <= 10; ++frame) {
if (rolls[frameIndex] == 10) {
score += 10 + rolls[frameIndex + 1] + rolls[frameIndex + 2];
++frameIndex;
} else if (isSpare(frameIndex)) {
score += spareBonus(frameIndex);
frameIndex += 2;
} else {
score += frameScore(frameIndex);
frameIndex += 2;
}
}
return score;
}
Forth Test
Blue
Refactors also kind of
write themselves!
@Test
public void oneStrike() {
rollStrike();
game.roll(3);
game.roll(4);
rollMany(17, 0);
assertThat(game.score(), is(24));
}
private void rollStrike() {
game.roll(10);
}
public int score() {
int score = 0;
int frameIndex = 0;
for (int frame = 1; frame <= 10; ++frame) {
if (isStrike(frameIndex)) {
score += strikeBonus(frameIndex);
++frameIndex;
} else if (isSpare(frameIndex)) {
score += spareBonus(frameIndex);
frameIndex += 2;
} else {
score += frameScore(frameIndex);
frameIndex += 2;
}
}
return score;
}
private boolean isStrike(int frameIndex) {
return rolls[frameIndex] == 10;
}
private int strikeBonus(int frameIndex) {
return 10 + rolls[frameIndex + 1] + rolls[frameIndex + 2];
}
So there's just one last case to test: making an Spare or an Strike on the last frame!
@Test
public void perfectGame() {
rollMany(12, 10);
assertThat(game.score(), is(300));
}
Fifth Test
Green
Our code already handles that case, but it was not easy to be certain!
Writing first the test did allow us to avoid making the code messier without additional functionality!
We are done!
If you are not sure, you can try to write a unit test that prove us false!
public int score() {
int score = 0;
int frameIndex = 0;
for (int frame = 1; frame <= 10; ++frame) {
if (isStrike(frameIndex)) {
score += strikeBonus(frameIndex);
++frameIndex;
} else if (isSpare(frameIndex)) {
score += spareBonus(frameIndex);
frameIndex += 2;
} else {
score += frameScore(frameIndex);
frameIndex += 2;
}
}
return score;
}
This may not be "perfect", but is really readable, much better than my attempt without using TDD
Do It Yourself
Palindrome
A palindrome is a sentence that reads the same backward as forward.
Palindrome
- "Radar"
- "Anna"
- "¡Anita, lava la tina!"
- "A man, a plan, a canal - Panama!"
- "" // empty String
There's one solution that has a complexity of O(n^2)
public static boolean isPalindrome(String s) {
// not really Java, but you get the idea!
return s.equals(s.reverse());
}
That algorithm stems
from the definition, but
we can do better. Maybe TDD can help us?
Palindrome
Use TDD to create
a function isPalindrome(String)
Don't let the previous algorithm cloud
your coding, let the
test guide you!
If you follow TDD
is impossible you end
using reverse()
Possible Tests
@Test
public void anEmptyStringIsPalindrome() {
return assertThat(isPalindrome(""), is(true));
}
@Test
public void aOneLetterStringIsPalindrome() {
return assertThat(isPalindrome("a"), is(true));
}
@Test
public void abIsNotPalindrome() {
return assertThat(isPalindrome("ab"), is(false));
}
@Test
public void radarIsPalindrome() {
return assertThat(isPalindrome("radar"), is(true));
}
@Test
public void notAllWordsThatStartAndEndWithSameCharacterArePalindrome() {
return assertThat(isPalindrome("reader"), is(true));
}
@Test
public void palindromeCheckingIsCaseInsensitive() {
return assertThat(isPalindrome("Radar"), is(true));
}
@Test
public void palindromeCheckingIgnoreWhiteSpace() {
return assertThat(isPalindrome("A man a plan a canal Panama"), is(true));
}
@Test
public void palindromeCheckingIgnoreNoneLetters() {
return assertThat(isPalindrome("A man, a plan, a canal - Panama"), is(true));
}
Conclusion
TDD
- Is just a tool
- Don't feel force to use it
- Not all solutions benefit from it
- You need to practice to feel comfortable using it - Kata
TDDish
- Even if you are
- Don't feel force to use it
- Not all solutions benefit from it
- You need to practice to feel comfortable using it - Kata
TDD
By Carlos Obregón
TDD
- 2,118