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
Made with Slides.com