Salta el contingut
 

Exemple: TDD – FizzBuzz

Autor: Joan Puigcerver Ibáñez

Correu electrònic: j.puigcerveribanez@edu.gva.es

Llicència: CC BY-NC-SA 4.0

(Reconeixement - NoComercial - CompartirIgual) 🅭

Objectius

Veure el procés de desenvolupament d'un problema utilitzant TDD.

Fizz Buzz

Ens han demanat implementar un programa que, donat un nombre, ens retorne els següents valors segons les condicions:

  • Si el nombre és divisible per 3, retornar Fizz.
  • Si el nombre és divisible per 5, retornar Buzz.
  • Si el nombre és divisible per 3 i per 5, retornar FizzBuzz.
  • En qualsevol altre cas, retornar el nombre original.
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.examples.tdd

La implementació es realitzarà en classe FizzBuzz en el mètode transform() mitjançant Desenvolupament Guiat per Tests (TDD).

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

  • 🔴 ROIG: Crear una prova que falle.
  • 🟢 VERD: Implementar el codi mínim perquè la prova passe.
  • 🔵 REFACTORITZAR: Refactoritzar el codi.

Iteració 1

🔴 ROIG

Creem la primera prova, en la qual comprovem que el mètode transform(1) ens retorna 1.

package ud4.examples.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.examples.tdd;

public class FizzBuzz {
}

Si executem la prova anterior, veurem que ja s'executa correctament.

🔵 REFACTORITZAR

De moment, no hi ha res a refactoritzar.

Iteració 2

🔴 ROIG

Creem la segona prova, en la qual comprovem 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

El primer pas per aconseguir que la prova passe és crear el mètode transform().

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

Encara no hi ha res a refactoritzar.

Iteració 3

🔴 ROIG

Creem la tercera prova, en la qual comprovem que el mètode transform() ha d'acceptar 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

El primer pas per aconseguir que la prova passe és afegir el paràmetre int number al mètode transform().

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

En aquest punt, si executem les proves, veurem que la prova passa, però l'anterior prova falla perquè el mètode transform() no pot ser invocat sense paràmetres, per tant, hem de decidir com volem procedir.

En aquest cas, com que la prova que falla és més antiga i prova un comportament que també estem comprovant amb la prova actual, decidim eliminar-la.

Nota

S'ha comentat en compte 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 de l'eliminació de la prova, no hi ha res a refactoritzar.

Iteració 4

🔴 ROIG

Creem la quarta prova, que provem el primer cas: el mètode transform(1) ens 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

El primer pas per aconseguir que la prova passe (🟢 VERD) és modificar el valor de retorn de la funció i retornar el valor 1 al mètode transform().

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

Encara no hi ha res a refactoritzar.

Iteració 5

🔴 ROIG

Creem la cinquena prova, que provem el segon cas: el mètode transform(2) ens 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, podem fer que la prova passe retornant el valor 2 al mètode transform().

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

Aquesta solució passa la prova que acabem de fer, però no passa la prova anterior, ja que si passem el valor 1 al mètode transform(), aquest retorna 2 en comptes de 1, per tant, la solució no és vàlida. Gràcies a haver fet la prova anterior, ens hem adonat que la solució no és vàlida i li hem 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ò ara ens podem plantejar si la solució és òptima o no.

En aquest cas, podem refactoritzar el codi perquè siga més llegible i funcione en el cas general:

  • En qualsevol altre cas, retornar el nombre original.
public class FizzBuzz {
    public int transform(int n) {
        return n;
    }
}

Iteració 6

🔴 ROIG

Passem al primer cas especial: Si el nombre és divisible per 3, retornar "Fizz", per tant, creem la prova:

@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 hem de canviar el tipus de retorn al mètode transform() i retornar un 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,i per tant, hem de refactoritzar 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

Hem de refactoritzar les proves anteriors perquè esperen un String.

Iteració 7

En aquesta iteració podríem 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 executem aquest test veurem que directament passa correctament i per tant, la funcionalitat que esperem d'aquest test ja ha segut provada anteriorment i no és necessari fer aquesta prova.

🔴 ROIG

Anem a provar 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, hem de 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

Anem a provar 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, hem de 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 cas, ens adonem que la condició n == 3 || n == 6 pot ser simplificada i generalitzada 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 han segut provats anteriorment, per tant, no és necessari fer aquestes proves.

🔴 ROIG

Anem a provar 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, hem de modificar el mètode transform() perquè retorne "Buzz" quan el paràmetre és 10.

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

En aquest cas ja hem fet servir el mòdul (%) per a comprovar si un nombre és divisible per un altre.

🔵 REFACTORITZAR

Generalització de la condició de Buzz.

Iteració 10

Els casos 11, 12, 13 i 14 ja han segut provats anteriorment, per tant, no és necessari fer aquestes proves.

🔴 ROIG

Anem a provar 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, hem de 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 executem aquesta prova veurem que falla, ja que el mètode transform() retorna "Fizz" quan el paràmetre és 15, en compte de "FizzBuzz". Això es deu a que la condició n % 3 == 0 és certa i per tant, retorna "Fizz" i no continua avaluant les altres condicions. Caldria canviar l'ordre de les condicions perquè la condició n == 15 es comprove abans que la condició 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 ens 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 (🟢 VERD), hem de 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

Podem optimitzar la condició n == 15 || n == 30 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

Implementació

FizzBuzz.java
package ud4.examples;

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.examples;

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.examples;

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 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);
    }

    @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

  • Hem vist el procés de desenvolupament d'un problema utilitzant TDD.
  • Hem comprovat la utilitat de les proves per comprovar el funcionament del nostre codi.
    • Hem detectat errors que potser no haguérem detectat sense les proves.
  • Les proves han guiats el nostre desenvolupament, que ens han aportat una utilitat immediata i ens han permés refactoritzar el codi.
  • Si s'afegeix una nova regla, podem afegir noves proves i modificar el codi amb la seguretat que la funcionalitat anterior seguirà sent correcta.
  • La classe FizzBuzzTest ens proporciona una documentació viva del comportament de la classe FizzBuzz.

Recursos i referències

Advertència

midudev | Curso de Test Driven Development desde Cero con JavaScript, React y Vitest: Aquest vídeo explica com fer TDD amb JavaScript, React i Vitest. No obstant això, el concepte de TDD és el mateix i és aplicable a qualsevol llenguatge de programació.