Hace unas semanas estuve en un Merendojo de Madrid Software Craftmanship (desde aquí gracias a los organizadores Luís, Pablo y el resto de personas) y después de la kata estuvimos discutiendo distintos enfoques sobre como afrontar el problema. En un momento de la conversación surgió el tema de como afrontar los tests, por donde empezar, desde dentro hacia afuera, haciendo test desde la parte más externa posible,… la verdad es que la charla fue bastante interesante y me picó la curiosidad sobre como afrontar los tests y como hacer TDD cuando tienes un problema grande. Así que buscando en Internet encontré este interesante post TDD – From the Inside Out or the Outside In? de Georgina Mcfadyen y me he animado a hacer una traducción libre.
A menudo la parte más difícil de programar es saber por donde empezar. Haciendo TDD (Test Driven Development) la mejor lugar de empezar es con un test, pero estar delante de una página en blanco puede ser desalentador.

¿La mejor manera de empezar es por los detalles de lo que está construyendo, y dejar que la arquitectura vaya emergiendo con un enfoque de adentro hacia afuera (Inside Out)? O bien, empezar por algo grande y dejar que los detalles vayan revelando a medida que se van usando desde afuera hacia adentro (Outside In)
Inside Out (Bottom Up/Chicago School/Aproximación Clásica)
Aunque todos los desarrolladores deben tener en cuenta la arquitectura, hacer TDD pensando de adentro hacia afuera (Inside Out) permite al desarrollador centrarse en cada cosa a su tiempo. Cada entidad (por ejemplo un modulo o una clase) se crea cuando la aplicación esté construida. En cierto sentido podría considerarse que las entidades individuales no aportan valor hasta que están unidas, y todo el sistema está enlazado en una etapa tardía lo que puede suponer un alto riesgo. Por otro lado, centrándonos en una única entidad cada vez hace que el desarrollo se paralelice dentro del equipo.
Tomando como ejemplo el juego del Tres en raya, una solución podría incluir al menos 3 entidades un Tablero, un Símbolo y un Juego.
Usando la aproximación de TDD de adentro hacia afuera (Inside Out), el Tablero y el Símbolo son fácilmente identificables por sí solas, mientras el Juego integra las entidades en todo el sistema.
Empezando con el Tablero, el desarrollador puede pensar en las funcionalidades para esta entidad e implementar estas responsabilidades con tests.
Por ejemplo, se necesitará el tablero para actualizar el movimiento de un jugador, por lo que es posible realizar una prueba como la siguiente.
class BoardTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function updateBoardWithUsersMove()
{
$board = new Board("- - - " .
"- - - " .
"- - -");
$updatedBoard = $board->update(1, 'X');
$this->assertEquals('X',$updatedBoard->position(1));
}
}
El tablero necesitará identificar las combinaciones ganadores, así que estos tests podrían añadirse uno a uno con el fin de «perforar» esta funcionalidad
/** @test */
public function hasWinningRow()
{
$board = new Board("X X X " .
"_ _ _ " .
"_ _ _");
$hasWinningLine = $board->hasWinningLine();
$this->assertEquals(true, $hasWinningLine);
}
/** @test */
public function hasWinningColumn()
{
$board = new Board("X _ _ " .
"X _ _ " .
"X _ _");
$hasWinningLine = $board->hasWinningLine();
$this->assertEquals(true, $hasWinningLine);
}
/** @test */
public function hasWinningDiagonal()
{
$board = new Board("X _ _ " .
"_ X _ " .
"X _ _");
$hasWinningLine = $board->hasWinningLine();
$this->assertEquals(true, $hasWinningLine);
}
Una ves que el Tablero se considere completo, se puede empezar con la siguiente entidad. El símbolo, no está relacionado con el Tablero, puede empezarse y el desarrollador puede testear que se muestra correctamente en la pantalla cuando se le pregunta al jugador por su movimiento.
class PromptTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function promptsForNextMove()
{
$writer = new StringWriter();
$prompt = new Prompt($writer);
$prompt->askForNextMove();
$this->assertEquals('Please enter move:', $writer);
}
}
El desarrollador tiende a abordar de manera aislada cada pieza de funcionalidad.
Cuando llegamos al el Juego, que es lo que está más arriba, debemos poner todas las piezas juntas, cada entidad individual debe interactuar con otra.
class GameTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function gameIsOverWhenWinningMoveMade()
{
$writer = new StringWriter();
$reader = new StringReader();
$prompt = new Prompt($reader, $writer);
$game = new Game($prompt, new Board("X _ _ " .
"_ X _ " .
"X _ _"));
$updatedBoard = $game->play();
$this->assertContains('Please enter move:', $writer);
$this->assertContains('Player X won!', $reader);
$this->assertEquals('X',$updatedBoard->posisiont(6));
}
}
Por lo cual hacer TDD utilizando la aproximación de TDD de adentro hacia afuera (Inside Out) se centra en realizar las entidades del sistema, el riesgo de que estas entidades no interactúen correctamente con otras «se empuja» a una etapa futura. Si las entidades no se comunican como se espera, habrá que rehacer el trabajo.
Del test de arriba, queda claro que es necesario imprimir un mensaje ganador en el prompt, donde se detalla que el jugador X o el O ganó el juego. Como el símbolo ganador no esta disponible en el Board, sería necesario implementar a posteriori una funcionalidad en Board que nos indique el símbolo ganador.
/** @test */
public function hasCorrectWinningSymbol()
{
$board = new Board("X _ _ " .
"X _ _ " .
"X _ _"));
$winningSymbol = $board->getWinningSymbol();
$this->assertEquals('X', $winningSymbol);
}
Sería necesario actualizar el Prompt para que proveyese de un método que toma este símbolo ganador, y anuncia al ganador.
/** @test */
public function displaysCongratulatoryMessage()
{
$writer = new StringWriter();
$reader = new StringReader();
$prompt = new Prompt($reader, $writer);
$prompt->displayWinningMessageForPlayer('X');
$this->assertEquals('Player X won!', $$writer);
}
Este ejemplo demuestra que cuando usamos TDD de dentro hacia fuera (Inside Out), no se requiere entender todo el diseño del sistema al principio. Solo es necesario identificar las entidades al comenzar. Los detalles intrínsecos de cada entidad van emergiendo al realizar sus tests unitarios. Llegando a un diseño que se asemeja bien al Principio de única Responsabilidad (Single Responsibility Principle SRP). En la fase inicial, no siempre esta claro que comportamiento necesita exponerse en una entidad. Esto podría resultar en exponer más o menos comportamiento que el necesario.
Con De adentro hacia Afuera Inside Out, en conjunto de entidades que expresan un comportamiento, como el caso de Game, los tests unitarios podrías sobrepasar más de una entidad (como pasa con Board y Prompt) Esto significa que las entidades colaboradoras pueden ser reemplazadas sin cambiar el test, proveyendo de un framework seguro para refactoring.
Por ejemplo, si se desea utilizar un PrettyWriter
en lugar de usar StringWriter
en Game, aparte de modificar la inyección cuando se instancia Game, no es necesario tocar el resto de tests.
/** @test */
public function gameIsOverWhenWinningMoveMade()
{
$writer = new PrettyWriter();
$reader = new StringReader();
$prompt = new Prompt($reader, $writer);
$game = new Game($prompt, new Board("X _ _ " .
"_ X _ " .
"X _ _"));
$updatedBoard = $game->play();
$this->assertContains('Player X won!', $writer);
}
Vale la pena señalar que encontrar bugs puede ser un poco más complicado con la aproximación de TDD de adentro hacia afuera (Inside Out) ya que es necesario revisar muchas entidades cuando se está investigando el asunto.
Finalmente, cuando se comienza con un nuevo lenguaje la aproximación de TDD de adentro hacia afuera (Inside Out) es un buen punto de partida. Los desarrolladores solo necesitan tener el foco en una entidad cada vez. Así pueden aprender y conocer mejor el lenguaje y el framework de test a medida que van avanzando. La aproximación de adentro hacia afuera (Inside Out) generalmente no necesita test dobles porque las entidades están todas identificadas desde el principio por lo que pueden utilizarse.
Outside In TDD (Top Down/London School/Mockist Approach)
La aproximación de TDD Desde afuera hacia adentro (Outside In) se presta bien a tener una ruta definida del sistema desde muy pronto, si se «hardcodean» algunas partes.
Los tests se basan en escenarios en los que las peticiones de usuario las entidades están conectadas desde el principio. Esto permite emerger con fluidez una API y probar la integración desde el principio del desarrollo.
Teniendo el foco en un flujo completo del sistema desde que empieza, conociendo como es necesario que interactuen las diferentes partes del sistema unas con otras. Como las entidades emergen, es necesario que sea realicen «mocks» o «stubs» para permitir que se descubran sus detalles más adelante. Esta aproximación hace que los desarrolladores necesiten conocer como testar interacciones desde el frente, ya sea con un framework de mocks o escribiendo nuestros propios test dobles. Los desarrolladores entonces volverán atrás, proveyendo la implementación real de los mocks o de las entidades «stub» una vez tengan los test unitarios.
Usando el ejemplo del Tres en Raya, las implementación comienza con gran test de aceptación que está fallando para el escenario «Dado que se realiza el movimiento ganador, el juego deberá mostrar el mensaje de ganador.»
class GameTest extends PHPUnit_Framework_TestCase
{
/** @test */
public function gameShouldEndIfThereIsAWinningRowInTheGrid()
{
$board = new Board("X X _ " .
"O _ O " .
"_ _ _");
$promptSpy = new PromptSpy(2);
$game = new Game($promptSpy, $board);
$game->play();
$this->assertTrue(true, promptSpy->hasAnnouncedWinner());
}
}
Este test dará como resultado la creación de muchas entidades, aunque inicialmente el resultado podría ser «harcodeado» con el resultado esperado
class Game
{
private $prompt;
public funtcion __construct(Prompt $prompt, Board $board)
{
this->prompt = $prompt;
}
public function play() {
$this->prompt->displaysWinningMessageFor('X');
}
}
Se aplaza la implementación de Prompt
para más adelante utilizando PromptSpy
en su lugar. La clase de test GameTest
puede verificar que el Prompt
se está comunicando correctamente. Así que una vez que la interfaz de Prompt
está definida, el desarrollador puede ponerse a escribir los tests unitarios de manera separada para la implementación real de Prompt
.
A menudo los frameworks de test no ofrecen funcionalidades de mock en sí mismo, y algunos desarrolladores prefieren escribir sus propios mocks en lugar de buscar un «framework» de mocks. Esto es una tarea sorprendentemente sencilla en lenguajes como Java, donde hay tipado stricto, pero puede ser algo más complicado en lenguajes dinámicos o funcionales.
class PromptSpy implements Prompt
{
$hasDisplayedWinningMessage = false;
public function __construct($playersMove)
{
}
public function displaysWinningMessage()
{
$hasDisplayedWinningMessage = true;
}
public function hasAnnouncedWinner() {
return $this->hasDisplayedWinningMessage;
}
}
El siguiente test muestra a jugadores haciendo movimientos una vez que se ha dibujado el juego. En este punto, es necesario implementar un poco de lógica, en la cuál el juego puede leer los movimientos de los jugadores.
/** @test */
public function playersTakeTurnsUntilTheGameIsDrawn()
{
$seriesOfMoves = "1\n2\n5\n3\n6\n4\n7\n9\n8\n";
$promptSpy = new PromptSpy($seriesOfMoves);
$board = new Board("_ _ _ " .
"_ _ _ " .
"_ _ _");
$game = new Game($promptSpy, $board);
$this->assertTrue(true, $promptSpy->hasAnnouncedDraw());
$this->assertEquals(9, $promptSpy->numberOfTimesPlayersPromptedForMove());
}
Esto quiere decir que `PrompSpy necesita actualizar un contador con el numero de jugadas que ha realizado un jugador, para estar seguro de que se ha mostrado el mensaje.
class PromptSpy implements Prompt {
private boolean hasDisplayedWinningMessage = false;
private boolean hasDisplayedDrawMessage = false;
private int numberOfTimesUsersPrompted = 0;
private final BufferedReader reader;
public PromptSpy(String playersMove) {
reader = new BufferedReader(new StringReader(playersMove));
}
public int read() {
try {
return Integer.valueOf(reader.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}
public void displaysWinningMessage() {
hasDisplayedWinningMessage = true;
}
public void displaysDrawMessage() {
hasDisplayedDrawMessage = true;
}
public void promptUserForMove() {
numberOfTimesUsersPrompted++;
}
public boolean hasAnnouncedWinner() {
return hasDisplayedWinningMessage;
}
public boolean hasAnnouncedDraw() {
return hasDisplayedDrawMessage;
}
public int numberOfTimesPlayersPromptedForMove() {
return numberOfTimesUsersPrompted;
}
}
Llegados hasta aquí, la clase Game podría ser algo como lo siguiente
class Game
{
private $board;
private $prompt;
public function __construct($prompt, $board)
{
$this->prompt = $prompt;
$this->board = $board;
}
public function play()
{
while ($this->board->hasFreeSpaces() && !$this->board->hasWinner()) {
$this->prompt->promptUserForMove();
$this->update($this->prompt->read(), $this->nextSymbol());
}
if ($this->board->hasWinner()) {
$this->prompt->displayWinningMessageForPlayer($this->board->getWinningSymbol());
} else {
$this->prompt->displayDrawMessage();
}
}
}
Como se puede ver a través de este ejemplo utilizando la aproximación de TDD De fuera hacia Adentro (Outside In) el desarrollador necesita un conocimiento previo de como las entidades van a comunicarse en el sistema. Poniendo el foco en como las entidades interactúan en vez de en los detalles internos se utilizan mucho los tes de aceptación. Las soluciones con la aproximación De fuera hacia Adentro (Outside In) a menudo aplican la Ley de Demeter – Nunca se crea una entidad sin que haya una entidad primaria que pregunte por ella – Esto se añade bién al principio «Dime no Preguntes», y da como resultado un estado en el el que solo se expone algo si es requerido por otra entidad.
Con la aproximación de TDD De fuera hacia Adentro (Outside In) los detalles de diseño están en los test. Por lo que un cambio de diseño se traduce en un cambio de los tests. Esto puede traer consigo mayor riesgo, o dar más confianza para implementarlo.
Conclusión
Ambas aproximaciones forman parte de la caja de herramientas de un desarrollador. Si se conocen bien el lenguaje de programación y el dominio del problema, entonces cualquier aproximación puede ser satisfactoria. Debido a que ambos enfoques necesitan buenos tests, que proporcionan un buen «arnés de seguridad» que ayuda a conseguir un diseño robusto.
La aproximación de TDD De adentro hacia afuera (Inside Out) favorece a los desarrolladores a los que les gusta ir construyendo pieza a pieza. Puede ser más sencillo para empezar, siguiendo un enfoque metódico para identificar las entidades, y trabajando duro para llevar a cabo el comportamiento interno. Con la aproximación de TDD De fuera hacia Adentro (Outside In) el desarrollador empieza a construir todo el sistema, y lo descompone en componentes más pequeños cuando se presentan oportunidades de refactorización. Esta ruta es más exploratorio, y es ideal en las situaciones donde hay una idea general del objetivo, pero los detalles finales de la implementación están menos claros.
En última instancia la decisión se reduce a la tarea en cuestión, las preferencias individuales, y la metodología que use el equipo con el que se colabora. Si los test son muy difíciles de configurar, podría ser una señal de que el diseño es demasiado complejo. Las entidades deberían revisarse continuamente para encontrar oportunidades de refactorización.
El objetivo de cada enfoque es el mismo, trabajar para construir un sistema robusto y fiable con un diseño mantenible.
Un comentario en “TDD – ¿De adentro hacia afuera o de fuera hacia adentro?”