Salta el contingut
 

Exemple: TDD – FizzBuzz

Joan Puigcerver Ibáñez

Llicència: CC BY-NC-SA 4.0

(Reconeixement - NoComercial - CompartirIgual) 🅭

Objectius

L'objectiu d'aquest exemple és mostrar el procés de desenvolupament d'un problema amb TDD.

FizzBuzz

Es vol implementar un programa que, donat un nombre, retorne un valor o un altre segons les condicions següents:

  • Si el nombre és divisible per 3, retorna Fizz.
  • Si el nombre és divisible per 5, retorna Buzz.
  • Si el nombre és divisible per 3 i per 5, retorna FizzBuzz.
  • En qualsevol altre cas, retorna el nombre original.

Aquests són els resultats esperats per als primers nombres:

1 -> 1
2 -> 2
3 -> Fizz
4 -> 4
5 -> Buzz
6 -> Fizz
7 -> 7
8 -> 8
9 -> Fizz
10 -> Buzz
11 -> 11
12 -> Fizz
13 -> 13
14 -> 14
15 -> FizzBuzz
16 -> 16

Implementació

ud4.tdd

La implementació es farà en el mètode transform() de la classe FizzBuzz, seguint el desenvolupament guiat per proves (TDD).

Per tant, el primer pas és crear la classe de proves FizzBuzzTest i començar a fer iteracions:

  1. 🔴 ROIG: crear una prova que falle.
  2. 🟢 VERD: implementar el codi mínim perquè la prova passe.
  3. 🔵 REFACTORITZAR: refactoritzar el codi.

Iteració 1

🔴 ROIG

Es crea la primera prova, que comprova que la classe FizzBuzz existeix.

package ud4.tdd;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class FizzBuzzTest {
    @Test
    @DisplayName("FizzBuzz class should exist")
    void fizzBuzzClassShouldExist() {
        FizzBuzz fizzBuzz = new FizzBuzz();
    }
}

Aquesta prova fallarà inicialment, ja que la classe FizzBuzz no existeix.

🟢 VERD

El primer pas per aconseguir que la prova passe és crear la classe FizzBuzz.

package ud4.tdd;

public class FizzBuzz {
}

Si s'executa la prova anterior, s'observa que ja passa correctament.

🔵 REFACTORITZAR

De moment, no hi ha res a refactoritzar.

Iteració 2

🔴 ROIG

Es crea la segona prova, que comprova que existeix el mètode transform().

@Test
@DisplayName("FizzBuzz::transform() method should exist")
void fizzBuzzTransformMethodShouldExist() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    fizzBuzz.transform();
}

Aquesta prova fallarà, ja que el mètode transform() no existeix.

🟢 VERD

Per aconseguir que la prova passe, cal crear el mètode transform().

public class FizzBuzz {
    public void transform() {
    }
}

🔵 REFACTORITZAR

Encara no hi ha res a refactoritzar.

Iteració 3

🔴 ROIG

Es crea la tercera prova, que comprova que el mètode transform() accepta un nombre enter com a paràmetre.

@Test
@DisplayName("FizzBuzz::transform() method should have an int parameter")
void fizzBuzzTransformMethodShouldHaveIntParameter() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    fizzBuzz.transform(1);
}

🟢 VERD

Per aconseguir que la prova passe, cal afegir el paràmetre int n al mètode transform().

public class FizzBuzz {
    public void transform(int n) {
    }
}

En aquest punt, si s'executen les proves, s'observa que la prova nova passa, però l'anterior falla, perquè el mètode transform() ja no es pot invocar sense paràmetres. Per tant, cal decidir com procedir.

En aquest cas, com que la prova que falla és més antiga i comprova un comportament que també comprova la prova actual, s'elimina.

La prova s'ha comentat en lloc d'eliminar-la perquè quede registrat el procés seguit.

public class FizzBuzzTest {
    @Test
    @DisplayName("FizzBuzz class should exist")
    void classFizzBuzzShouldExist() {
        FizzBuzz fizzBuzz = new FizzBuzz();
    }

    /*
    @Test
    @DisplayName("FizzBuzz::transform() method should exist")
    void fizzBuzzTransformMethodShouldExist() {
        FizzBuzz fizzBuzz = new FizzBuzz();
        fizzBuzz.transform();
    }
     */

    @Test
    @DisplayName("FizzBuzz::transform() method should have an int parameter")
    void fizzBuzzTransformMethodShouldHaveIntParameter() {
        FizzBuzz fizzBuzz = new FizzBuzz();
        fizzBuzz.transform(1);
    }
}

En aquest punt, totes les proves passen.

🔵 REFACTORITZAR

A banda d'eliminar la prova, no hi ha res a refactoritzar.

Iteració 4

🔴 ROIG

Es crea la quarta prova, que comprova el primer cas: transform(1) retorna 1.

@Test
@DisplayName("FizzBuzz::transform(1) should return 1")
void fizzBuzzTransform_givenValue1_shouldReturn1() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    int result = fizzBuzz.transform(1);
    assertEquals(1, result);
}

🟢 VERD

Per aconseguir que la prova passe, cal modificar el tipus de retorn del mètode transform() i retornar el valor 1.

public class FizzBuzz {
    public int transform(int n) {
        return 1;
    }
}

🔵 REFACTORITZAR

Encara no hi ha res a refactoritzar.

Iteració 5

🔴 ROIG

Es crea la cinquena prova, que comprova el segon cas: transform(2) retorna 2.

@Test
@DisplayName("FizzBuzz::transform(2) should return 2")
void fizzBuzzTransform_givenValue2_shouldReturn2() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    int result = fizzBuzz.transform(2);
    assertEquals(2, result);
}

🟢 VERD

En aquest cas, la prova passa si el mètode transform() retorna el valor 2.

public class FizzBuzz {
    public int transform(int n) {
        return 2;
    }
}

Aquesta solució passa la prova nova, però no l'anterior: si es passa el valor 1 al mètode transform(), retorna 2 en lloc de 1. Per tant, la solució no és vàlida. Gràcies a la prova anterior, s'ha detectat que la solució no és vàlida, i així s'ha tret rendiment a la prova escrita.

Una solució que sí que funciona per a les dues proves és la següent:

public class FizzBuzz {
    public int transform(int n) {
        if (n == 2)
            return 2;
        return 1;
    }
}

🔵 REFACTORITZAR

Totes les proves passen, però cal plantejar-se si la solució és òptima.

En aquest cas, es pot refactoritzar el codi perquè siga més llegible i funcione en el cas general (en qualsevol altre cas, retorna el nombre original):

public class FizzBuzz {
    public int transform(int n) {
        return n;
    }
}

Iteració 6

🔴 ROIG

A continuació, es prova el primer cas especial: si el nombre és divisible per 3, retorna Fizz. Per tant, es crea la prova següent:

@Test
@DisplayName("FizzBuzz::transform(3) should return Fizz")
void fizzBuzzTransform_givenValue3_shouldReturnFizz() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    String result = fizzBuzz.transform(3);
    assertEquals("Fizz", result);
}

🟢 VERD

En aquest cas, perquè funcione, cal canviar el tipus de retorn del mètode transform() a String.

public class FizzBuzz {
    public String transform(int n) {
        if (n == 3)
            return "Fizz";
        return String.valueOf(n);
    }
}

Aquest canvi fa que les proves anteriors fallen, ja que esperen un int i ara reben un String. Per tant, cal modificar les proves anteriors perquè esperen un String:

@Test
@DisplayName("FizzBuzz::transform(1) should return 1")
void fizzBuzzTransform_givenValue1_shouldReturn1() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    String result = fizzBuzz.transform(1);
    assertEquals("1", result);
}

🔵 REFACTORITZAR

A banda d'adaptar les proves anteriors, no hi ha res a refactoritzar.

Iteració 7

En aquesta iteració es podria comprovar el cas 4, que hauria de retornar 4.

@Test
@DisplayName("FizzBuzz::transform(4) should return 4")
void fizzBuzzTransform_givenValue4_shouldReturn4() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    String result = fizzBuzz.transform(4);
    assertEquals("4", result);
}

No obstant això, si s'executa aquesta prova, s'observa que passa directament. Per tant, la funcionalitat que comprova ja s'ha provat anteriorment i no cal afegir-la.

🔴 ROIG

Es prova el cas 5, que hauria de retornar Buzz.

@Test
@DisplayName("FizzBuzz::transform(5) should return Buzz")
void fizzBuzzTransform_givenValue5_shouldReturnBuzz() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    String result = fizzBuzz.transform(5);
    assertEquals("Buzz", result);
}

🟢 VERD

Per aconseguir que la prova passe, cal modificar el mètode transform() perquè retorne Buzz quan el paràmetre és 5.

public String transform(int n) {
    if (n == 3)
        return "Fizz";
    if (n == 5)
        return "Buzz";
    return String.valueOf(n);
}

🔵 REFACTORITZAR

No hi ha res a refactoritzar.

Iteració 8

🔴 ROIG

Es prova el cas 6, que hauria de retornar Fizz.

@Test
@DisplayName("FizzBuzz::transform(6) should return Fizz")
void fizzBuzzTransform_givenValue6_shouldReturnFizz() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    String result = fizzBuzz.transform(6);
    assertEquals("Fizz", result);
}

🟢 VERD

Per aconseguir que la prova passe, cal modificar el mètode transform() perquè retorne Fizz quan el paràmetre és 6.

public String transform(int n) {
    if (n == 3 || n == 6)
        return "Fizz";
    if (n == 5)
        return "Buzz";
    return String.valueOf(n);
}

🔵 REFACTORITZAR

En aquest punt, s'observa que la condició n == 3 || n == 6 es pot simplificar i generalitzar a n % 3 == 0.

public String transform(int n) {
    if (n % 3 == 0)
        return "Fizz";
    if (n == 5)
        return "Buzz";
    return String.valueOf(n);
}

Iteració 9

Els casos 7, 8 i 9 ja estan coberts per les proves anteriors i, per tant, no cal provar-los.

🔴 ROIG

Es prova el cas 10, que hauria de retornar Buzz.

@Test
@DisplayName("FizzBuzz::transform(10) should return Buzz")
void fizzBuzzTransform_givenValue10_shouldReturnBuzz() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    String result = fizzBuzz.transform(10);
    assertEquals("Buzz", result);
}

🟢 VERD

Per aconseguir que la prova passe, cal modificar el mètode transform() perquè retorne Buzz quan el paràmetre és 10. Seguint el raonament de la iteració anterior, s'utilitza directament el mòdul (%) per comprovar si el nombre és divisible per 5.

public String transform(int n) {
    if (n % 3 == 0)
        return "Fizz";
    if (n % 5 == 0)
        return "Buzz";
    return String.valueOf(n);
}

🔵 REFACTORITZAR

No hi ha res a refactoritzar, ja que la condició de Buzz s'ha generalitzat en el pas anterior.

Iteració 10

Els casos 11, 12, 13 i 14 ja estan coberts per les proves anteriors i, per tant, no cal provar-los.

🔴 ROIG

Es prova el cas 15, que hauria de retornar FizzBuzz.

@Test
@DisplayName("FizzBuzz::transform(15) should return FizzBuzz")
void fizzBuzzTransform_givenValue15_shouldReturnFizzBuzz() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    String result = fizzBuzz.transform(15);
    assertEquals("FizzBuzz", result);
}

🟢 VERD

Per aconseguir que la prova passe, cal modificar el mètode transform() perquè retorne FizzBuzz quan el paràmetre és 15.

public String transform(int n) {
    if (n % 3 == 0)
        return "Fizz";
    if (n % 5 == 0)
        return "Buzz";
    if (n == 15)
        return "FizzBuzz";
    return String.valueOf(n);
}

Si s'executa aquesta prova, s'observa que falla, ja que el mètode transform() retorna Fizz quan el paràmetre és 15, en lloc de FizzBuzz. Això es deu al fet que la condició n % 3 == 0 és certa i, per tant, retorna Fizz sense avaluar la resta de condicions. Cal canviar l'ordre de les condicions perquè la condició n == 15 es comprove abans que n % 3 == 0:

public String transform(int n) {
    if (n == 15)
        return "FizzBuzz";
    if (n % 3 == 0)
        return "Fizz";
    if (n % 5 == 0)
        return "Buzz";
    return String.valueOf(n);
}

🔵 REFACTORITZAR

No hi ha res a refactoritzar.

Iteració 11

🔴 ROIG

L'últim cas que queda per comprovar és el 30, que hauria de retornar FizzBuzz.

@Test
@DisplayName("FizzBuzz::transform(30) should return FizzBuzz")
void fizzBuzzTransform_givenValue30_shouldReturnFizzBuzz() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    String result = fizzBuzz.transform(30);
    assertEquals("FizzBuzz", result);
}

🟢 VERD

Per aconseguir que la prova passe, cal modificar el mètode transform() perquè retorne FizzBuzz quan el paràmetre és 30.

public String transform(int n) {
    if (n == 15 || n == 30)
        return "FizzBuzz";
    if (n % 3 == 0)
        return "Fizz";
    if (n % 5 == 0)
        return "Buzz";
    return String.valueOf(n);
}

🔵 REFACTORITZAR

La condició n == 15 || n == 30 es pot generalitzar a n % 15 == 0.

public String transform(int n) {
    if (n % 15 == 0)
        return "FizzBuzz";
    if (n % 3 == 0)
        return "Fizz";
    if (n % 5 == 0)
        return "Buzz";
    return String.valueOf(n);
}

Resultat final

Després de totes les iteracions, la implementació i les proves queden de la manera següent.

Implementació

FizzBuzz.java
package ud4.tdd;

public class FizzBuzz {
    public String transform(int n){
        if (n % 15 == 0)
            return "FizzBuzz";
        else if (n % 3 == 0)
            return "Fizz";
        else if (n % 5 == 0)
            return "Buzz";

        return String.valueOf(n);
    }
}

Proves

FizzBuzzTest.java
package ud4.tdd;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class FizzBuzzTest {
    @Test
    @DisplayName("FizzBuzz class should exist")
    void fizzBuzzClassShouldExist(){
        FizzBuzz fizzBuzz = new FizzBuzz();
    }

    /*
    @Test
    @DisplayName("FizzBuzz::transform() method should exist")
    void fizzBuzzTransformMethodShouldExist(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        fizzBuzz.transform();
    }
    */

    @Test
    @DisplayName("FizzBuzz::transfrom() should have an int parameter")
    void fizzBuzzTransformShouldHaveIntParameter(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        fizzBuzz.transform(1);
    }

    @Test
    @DisplayName("FizzBuzz::transform(1) should return 1")
    void fizzBuzzTransform_given1_shouldReturn1(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        String result = fizzBuzz.transform(1);
        assertEquals("1", result);
    }

    @Test
    @DisplayName("FizzBuzz::transform(2) should return 2")
    void fizzBuzzTransform_given2_shouldReturn2(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        String result = fizzBuzz.transform(2);
        assertEquals("2", result);
    }

    @Test
    @DisplayName("FizzBuzz::transform(3) should return Fizz")
    void fizzBuzzTransform_given3_shouldReturnFizz(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        String result = fizzBuzz.transform(3);
        assertEquals("Fizz", result);
    }

    @Test
    @DisplayName("FizzBuzz::transform(5) should return Buzz")
    void fizzBuzzTransform_given5_shouldReturnBuzz(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        String result = fizzBuzz.transform(5);
        assertEquals("Buzz", result);
    }

    @Test
    @DisplayName("FizzBuzz::transform(6) should return Fizz")
    void fizzBuzzTransform_given6_shouldReturnFizz(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        String result = fizzBuzz.transform(6);
        assertEquals("Fizz", result);
    }

    @Test
    @DisplayName("FizzBuzz::transform(10) should return Buzz")
    void fizzBuzzTransform_given10_shouldReturnBuzz(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        String result = fizzBuzz.transform(10);
        assertEquals("Buzz", result);
    }

    @Test
    @DisplayName("FizzBuzz::transform(15) should return FizzBuzz")
    void fizzBuzzTransform_given15_shouldReturnFizzBuzz(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        String result = fizzBuzz.transform(15);
        assertEquals("FizzBuzz", result);
    }

    @Test
    @DisplayName("FizzBuzz::transform(30) should return FizzBuzz")
    void fizzBuzzTransform_given30_shouldReturnFizzBuzz(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        String result = fizzBuzz.transform(30);
        assertEquals("FizzBuzz", result);
    }
}
FizzBuzzParametizedTest.java
package ud4.tdd;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class FizzBuzzParametizedTest {
    @Test
    @DisplayName("FizzBuzz class should exist")
    void fizzBuzzClassShouldExist(){
        FizzBuzz fizzBuzz = new FizzBuzz();
    }

    /*
    @Test
    @DisplayName("FizzBuzz::transform() method should exist")
    void fizzBuzzTransformMethodShouldExist(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        fizzBuzz.transform();
    }
    */

    @Test
    @DisplayName("FizzBuzz::transfrom() should have an int parameter")
    void fizzBuzzTransformShouldHaveIntParameter(){
        FizzBuzz fizzBuzz = new FizzBuzz();
        fizzBuzz.transform(1);
    }

    @ParameterizedTest(name = "FizzBuzz::transform({0}) should return {1}")
    @DisplayName("FizzBuzz::transform()")
    @CsvSource({
            "1, 1",
            "2, 2",
            "3, Fizz",
            "5, Buzz",
            "6, Fizz",
            "10, Buzz",
            "15, FizzBuzz",
            "30, FizzBuzz"
    })
    void fizzBuzzTransform(int input, String expected){
        FizzBuzz fizzBuzz = new FizzBuzz();
        String result = fizzBuzz.transform(input);
        assertEquals(expected, result);
    }
}

Conclusions

Aquest exemple ha mostrat el procés de desenvolupament d'un problema amb TDD. D'aquest procés es poden extraure les conclusions següents:

  • Utilitat de les proves: les proves permeten comprovar el funcionament del codi i detectar errors que potser no s'haurien detectat sense elles.
  • Desenvolupament guiat: les proves han guiat el desenvolupament, han aportat una utilitat immediata i han permés refactoritzar el codi amb seguretat.
  • Evolució segura: si s'afig una regla nova, es poden afegir proves noves i modificar el codi amb la seguretat que la funcionalitat anterior continuarà sent correcta.
  • Documentació viva: la classe FizzBuzzTest documenta el comportament de la classe FizzBuzz.

Recursos i referències

📌 Aquest document pot quedar desactualitzat després d’imprimir-lo. Pots consultar la versió més recent a la pàgina web.
🌿 Abans d’imprimir aquest document, considera si és realment necessari. Redueix el consum de paper i ajuda a protegir el nostre entorn.