Una kata TDD (con código) en PHP: la secuencia de Fibonacci

En el vibrante mundo del desarrollo de software, la calidad del código, la robustez y la capacidad de mantener y extender nuestras aplicaciones son preocupaciones constantes. A menudo, nos encontramos inmersos en la vorágine de entregar funcionalidades, dejando de lado prácticas que, a la larga, resultan ser pilares fundamentales para un proyecto exitoso. Es en este contexto donde metodologías como el Desarrollo Guiado por Pruebas (TDD) y técnicas como las Katas de Programación adquieren una relevancia capital. TDD no es solo una forma de escribir pruebas, sino una disciplina de diseño que moldea la forma en que pensamos y construimos nuestro software. Las Katas, por su parte, son el gimnasio mental donde podemos perfeccionar nuestras habilidades, practicar nuevos enfoques y cimentar buenos hábitos de forma segura y repetitiva.

Este post busca sumergirte en una experiencia práctica: una kata TDD en PHP, utilizando la clásica secuencia de Fibonacci como nuestro campo de entrenamiento. Más allá de la implementación final, el verdadero valor reside en el viaje, en la iteración a través del ciclo "Rojo-Verde-Refactor" y en la comprensión de cómo cada pequeña prueba nos guía hacia un diseño más limpio y una solución más sólida. Prepara tu entorno PHP, un editor de código y una mente abierta; la práctica deliberada es la clave para la maestría.

¿Qué es TDD? El ciclo rojo-verde-refactor

Two people enjoy a peaceful sunset on Batumi's rocky coast, embodying tranquility and connection.

El Desarrollo Guiado por Pruebas (TDD) es una metodología de desarrollo de software en la que se escribe una prueba fallida antes de escribir el código funcional que hará que esa prueba pase. No se trata solo de pruebas unitarias, aunque estas son su herramienta principal; TDD es una disciplina que influye directamente en el diseño y la arquitectura del software. Su ciclo se resume en tres fases, a menudo recordadas por sus colores:

  1. Rojo (Red): Escribir una prueba automatizada para una pequeña porción de funcionalidad que aún no existe. Esta prueba, por definición, debe fallar. Es crucial que falle por la razón esperada, es decir, porque la funcionalidad no está implementada, no por un error en la prueba misma. Este paso nos obliga a pensar en la interfaz pública de nuestra clase o función antes de pensar en su implementación interna. Nos ayuda a definir qué queremos que haga el código.

  2. Verde (Green): Escribir el código mínimo necesario para que la prueba que acaba de fallar, y solo esa prueba, pase. El objetivo aquí no es escribir el código "perfecto" o la solución final, sino simplemente pasar la prueba de la manera más rápida y sencilla posible. Esto a menudo lleva a soluciones "feas" o incompletas, pero eso está bien, porque el siguiente paso las corregirá.

  3. Refactorizar (Refactor): Una vez que todas las pruebas están en verde, es el momento de mejorar la calidad del código. Esto incluye eliminar duplicidades, mejorar la legibilidad, optimizar algoritmos, aplicar patrones de diseño, etc., todo ello sin cambiar el comportamiento externo y con la confianza de que las pruebas unitarias existentes detectarán cualquier regresión. Este paso es fundamental para mantener una base de código limpia y sostenible.

Este ciclo se repite continuamente, en pequeños incrementos. Cada iteración nos acerca a una solución robusta y bien diseñada, respaldada por un conjunto de pruebas que actúan como una red de seguridad, permitiendo cambios futuros con mayor confianza. Personalmente, encuentro que TDD no solo reduce los errores, sino que me ayuda a pensar de forma más estructurada sobre el problema que estoy resolviendo, lo que a menudo lleva a un diseño más elegante.

Para más detalles sobre TDD, recomiendo el artículo seminal de Martin Fowler: Test-Driven Development.

La esencia de las katas de programación

El término "kata" proviene de las artes marciales japonesas, donde se refiere a una secuencia de movimientos practicada repetidamente para perfeccionar una técnica o un conjunto de habilidades. En el contexto de la programación, una kata es un ejercicio de programación pequeño y autocontenido que se practica con frecuencia. El objetivo no es simplemente resolver el problema una vez, sino resolverlo muchas veces, explorando diferentes enfoques, refactorizaciones y patrones.

Las katas de programación, popularizadas por Dave Thomas (uno de los autores de "The Pragmatic Programmer"), buscan ayudarnos a:

  • Practicar nuevas herramientas o lenguajes: Familiarizarnos con su sintaxis y paradigmas.
  • Mejorar habilidades existentes: Como el diseño de software, la escritura de código limpio o el uso de TDD.
  • Aprender nuevos patrones de diseño o refactorizaciones: Aplicándolos en un entorno de bajo riesgo.
  • Desarrollar fluidez y velocidad: Al enfrentarnos repetidamente a problemas similares.

La belleza de una kata es que no hay presión por la "solución final". El valor está en el proceso, en la repetición y en la reflexión sobre cómo mejorar con cada intento. Es una forma de "entrenamiento mental" para programadores.

Puedes leer más sobre las katas de programación en el sitio web de Dave Thomas: Code Kata.

Beneficios de combinar TDD y katas

Cuando combinamos TDD con las katas de programación, creamos un entorno de aprendizaje increíblemente potente. Los beneficios son múltiples:

  1. Refuerzo del ciclo TDD: Las katas proporcionan un escenario ideal para practicar el ciclo Rojo-Verde-Refactor en un contexto acotado, permitiendo que esta metodología se convierta en una segunda naturaleza.
  2. Mejora del diseño: Al escribir pruebas primero, somos forzados a pensar en la interfaz de nuestras clases y funciones. Esto, repetido en una kata, agudiza nuestra capacidad para diseñar APIs claras y concisas.
  3. Confianza en la refactorización: La existencia de un conjunto de pruebas robustas nos da la seguridad necesaria para refactorizar sin miedo, sabiendo que cualquier cambio que altere el comportamiento esperado será detectado. Esto es crucial para mantener la calidad del código a lo largo del tiempo.
  4. Desarrollo de habilidades de resolución de problemas: Las katas nos exponen a problemas en un entorno controlado, donde podemos experimentar con diferentes soluciones y aprender de nuestros errores sin consecuencias en un proyecto real.
  5. Adquisición de un flujo de trabajo disciplinado: La práctica constante con TDD en katas ayuda a internalizar un flujo de trabajo disciplinado, lo cual es invaluable en cualquier proyecto profesional.

Preparando el terreno: configuración del entorno

Antes de sumergirnos en el código, necesitamos configurar un entorno mínimo para ejecutar nuestras pruebas PHPUnit. Supondremos que ya tienes PHP instalado en tu sistema.

Instalación y configuración de PHPUnit

La forma más sencilla de instalar PHPUnit es a través de Composer, el gestor de dependencias de PHP. Si no tienes Composer, puedes descargarlo de su sitio oficial: Composer.

  1. Crea un nuevo directorio para tu proyecto:

    mkdir fibonacci-kata
    cd fibonacci-kata
    
  2. Inicializa Composer y agrega PHPUnit como dependencia de desarrollo:

    composer init
    # Sigue las instrucciones, puedes dejar la mayoría de los valores por defecto.
    # Cuando te pregunte por dependencias, omítelo.
    # Cuando te pregunte por dependencias de desarrollo, escribe 'phpunit/phpunit' y presiona Enter.
    # Luego, selecciona la versión más reciente (ej. '9.x-dev' o '9.5.x').
    # Una vez terminado, tu archivo composer.json debería lucir similar a esto:
    
    {
        "name": "tu-usuario/fibonacci-kata",
        "description": "Una kata TDD para Fibonacci",
        "type": "project",
        "license": "MIT",
        "autoload": {
            "psr-4": {
                "App\\": "src/"
            }
        },
        "require-dev": {
            "phpunit/phpunit": "^9.5"
        }
    }
    
  3. Instala las dependencias:

    composer install
    
  4. Crea un archivo de configuración de PHPUnit (phpunit.xml o phpunit.xml.dist) en la raíz del proyecto:

    <?xml version="1.0" encoding="UTF-8"?>
    <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
             bootstrap="vendor/autoload.php"
             colors="true"
             cacheResult="false"
             displayDetailsOnTestsThatTriggerNotices="true"
             displayDetailsOnTestsThatTriggerWarnings="true"
             displayDetailsOnTestsThatTriggerErrors="true">
        <testsuites>
            <testsuite name="Application">
                <directory>tests</directory>
            </testsuite>
        </testsuites>
    </phpunit>
    

    Este archivo le dice a PHPUnit dónde encontrar nuestras pruebas (en el directorio tests) y cómo cargar las clases (usando vendor/autoload.php de Composer).

Estructura del proyecto

Necesitaremos dos directorios principales: src para nuestro código fuente y tests para nuestras pruebas.

fibonacci-kata/
├── src/
├── tests/
├── vendor/
├── composer.json
├── composer.lock
└── phpunit.xml

Ahora estamos listos para comenzar con la kata.

Desarrollando la secuencia de Fibonacci con TDD

La secuencia de Fibonacci es una serie matemática donde cada número es la suma de los dos anteriores, comenzando con 0 y 1. La secuencia típicamente comienza así: 0, 1, 1, 2, 3, 5, 8, 13, 21, ...

Formalmente, se define como:

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2) para n > 1

Vamos a implementar una función que calcule el enésimo número de Fibonacci utilizando TDD.

Paso 1: Rojo - escribir una prueba que falla

Comenzamos con la prueba más simple posible. ¿Cuál es el primer caso de Fibonacci que podemos definir? F(0) = 0.

Crea el archivo tests/FibonacciTest.php:

<?php

namespace Tests;

use PHPUnit\Framework\TestCase;
use App\Fibonacci; // Necesitaremos esta clase

class FibonacciTest extends TestCase
{
    /** @test */
    public function it_returns_zero_for_zero()
    {
        $fibonacci = new Fibonacci();
        $this->assertEquals(0, $fibonacci->calculate(0));
    }
}

Ahora, ejecuta PHPUnit desde la raíz de tu proyecto:

./vendor/bin/phpunit

Deberías ver un error, algo como Class 'App\Fibonacci' not found. ¡Esto es genial! Es una prueba que falla por la razón correcta: la clase Fibonacci no existe. Esto es nuestro "Rojo".

Paso 2: Verde - escribir el código mínimo para que la prueba pase

Ahora, creamos la clase Fibonacci en src/Fibonacci.php con el código mínimo para que la prueba anterior pase.

<?php

namespace App;

class Fibonacci
{
    public function calculate(int $n): int
    {
        return 0; // Código mínimo para que F(0) = 0 pase
    }
}

Ejecuta PHPUnit de nuevo:

./vendor/bin/phpunit

¡Deberías ver que la prueba pasa! Es nuestro "Verde". Ahora, a refactorizar... oh, espera, no hay mucho que refactorizar aún. El código es trivial.

Volvemos al rojo. ¿Cuál es el siguiente caso? F(1) = 1.

Añadimos un nuevo método de prueba a tests/FibonacciTest.php:

// ...
class FibonacciTest extends TestCase
{
    // ...
    /** @test */
    public function it_returns_one_for_one()
    {
        $fibonacci = new Fibonacci();
        $this->assertEquals(1, $fibonacci->calculate(1));
    }
}

Ejecuta PHPUnit. ¡Rojo! La prueba it_returns_one_for_one falla, porque calculate(1) devuelve 0.

Ahora, hacemos que pase. Modifica src/Fibonacci.php:

<?php

namespace App;

class Fibonacci
{
    public function calculate(int $n): int
    {
        if ($n === 0) {
            return 0;
        }

        if ($n === 1) {
            return 1; // Añadido para pasar la nueva prueba
        }

        return 0; // Sigue siendo la base para otros casos
    }
}

Ejecuta PHPUnit. ¡Verde! Ambas pruebas pasan.

Refactorizamos. En este punto, no hay mucha complejidad que refactorizar. Podríamos pensar en una estructura switch o if/else if, pero para dos casos simples, el actual está bien.

Sigamos: F(2) = F(1) + F(0) = 1 + 0 = 1.

Añadimos a tests/FibonacciTest.php:

// ...
    /** @test */
    public function it_returns_one_for_two()
    {
        $fibonacci = new Fibonacci();
        $this->assertEquals(1, $fibonacci->calculate(2));
    }

Ejecuta PHPUnit. ¡Rojo!

Modifica src/Fibonacci.php:

<?php

namespace App;

class Fibonacci
{
    public function calculate(int $n): int
    {
        if ($n === 0) {
            return 0;
        }

        if ($n === 1) {
            return 1;
        }

        if ($n === 2) { // Añadimos este caso específico
            return 1;
        }

        return 0;
    }
}

Ejecuta PHPUnit. ¡Verde!

Refactorizamos. Aquí empezamos a ver una oportunidad. La definición recursiva de Fibonacci es F(n) = F(n-1) + F(n-2). Podríamos empezar a aplicar esto.

Ahora, vamos por F(3) = F(2) + F(1) = 1 + 1 = 2.

Añadimos a tests/FibonacciTest.php:

// ...
    /** @test */
    public function it_returns_two_for_three()
    {
        $fibonacci = new Fibonacci();
        $this->assertEquals(2, $fibonacci->calculate(3));
    }

Ejecuta PHPUnit. ¡Rojo!

Ahora es el momento de aplicar la lógica recursiva. Modifica src/Fibonacci.php:

<?php

namespace App;

class Fibonacci
{
    public function calculate(int $n): int
    {
        if ($n === 0) {
            return 0;
        }

        if ($n === 1) {
            return 1;
        }

        // Aplicamos la definición recursiva para n > 1
        return $this->calculate($n - 1) + $this->calculate($n - 2);
    }
}

Ejecuta PHPUnit. ¡Verde! ¡Todas las pruebas pasan!

Paso 3: Refactorizar - mejorar el código sin alterar su comportamiento

Ahora que tenemos una solución funcional y todas nuestras pruebas en verde, es hora de refactorizar.

El código actual es una implementación recursiva directa de Fibonacci. Es elegante, pero puede ser ineficiente para números grandes debido a las llamadas repetidas a la misma función (problema de superposición de subproblemas).

Podríamos mejorarla usando memorización (almacenando los resultados ya calculados) o, de forma más común, una implementación iterativa.

Refactorización a un enfoque iterativo:

<?php

namespace App;

class Fibonacci
{
    public function calculate(int $n): int
    {
        if ($n < 0) {
            throw new \InvalidArgumentException("El número de Fibonacci no puede ser negativo.");
        }

        if ($n === 0) {
            return 0;
        }

        if ($n === 1) {
            return 1;
        }

        $a = 0;
        $b = 1;
        for ($i = 2; $i <= $n; $i++) {
            $temp = $a + $b;
            $a = $b;
            $b = $temp;
        }

        return $b;
    }
}

Después de la refactorización, ¡ejecuta las pruebas de nuevo!

./vendor/bin/phpunit

¡Verde! Esto nos da la confianza de que, aunque hemos cambiado radicalmente la implementación interna, el comportamiento externo de la función calculate sigue siendo el mismo. Esta es la belleza de TDD. La refactorización se vuelve una actividad segura y placentera.

Ahora podemos añadir más pruebas para números más grandes y asegurarnos de que el algoritmo iterativo también funciona correctamente.

// ... en tests/FibonacciTest.php
    /** @test */
    public function it_returns_five_for_five()
    {
        $fibonacci = new Fibonacci();
        $this->assertEquals(5, $fibonacci->calculate(5));
    }

    /** @test */
    public function it_returns_eight_for_six()
    {
        $fibonacci = new Fibonacci();
        $this->assertEquals(8, $fibonacci->calculate(6));
    }

    /** @test */
    public function it_returns_large_numbers_correctly()
    {
        $fibonacci = new Fibonacci();
        $this->assertEquals(55, $fibonacci->calculate(10));
        $this->assertEquals(6765, $fibonacci->calculate(20));
    }

Todas estas pruebas deberían pasar con nuestra implementación iterativa. Si alguna fallara, sabríamos que hemos introducido una regresión durante la refactorización.

Ampliando nuestra kata: consideraciones avanzadas

Una vez completada la implementación básica de Fibonacci, las katas nos invitan a ir más allá.

Manejo de entradas inválidas

¿Qué pasa si n es un número negativo? La secuencia de Fibonacci no está definida para números negativos en su forma estándar. Podríamos decidir lanzar una excepción.

Primero, la prueba (en tests/FibonacciTest.php):

// ...
    /** @test */
    public function it_throws_an_exception_for_negative_input()
    {
        $fibonacci = new Fibonacci();
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage("El número de Fibonacci no puede ser negativo.");
        $fibonacci->calculate(-1);
    }

Ejecuta PHPUnit. ¡Rojo! (Esto asume que aún no hemos modificado calculate para lanzar la excepción).

Ahora, el código para hacer que pase (en src/Fibonacci.php):