Skip to content

danielribes/SnakesAndLadders

Repository files navigation

Introducción

Queria estrenarme con BDD (Behavior Drive Development) en PHP y esta Kata de Snakes And Ladders es una buena practica para ello en la que las necesidades de negocio descritas como historias de usuario marcan el flujo de tests con los que se va resolviendo.

En PHP esto se puede llevar a cabo usando el framework Behat, que trabaja a partir de historias de usuario (Features) y test de aceptación (Scenarios) descritos con lenguaje Gherkin.

También he usado PHPUnit que es el framework habitual en PHP para test unitarios y TDD. En este caso Behat guía el desarrollo de cada Feature y PHPUnit da soporte en aplicar tests unitarios a determinados elementos.

Estructura del proyecto

Esta versión solo resuelve la Feature 1 de la kata original pero es suficiente para ver como trabajar con Behat y BDD que al final es mi objetivo en este proyecto.

A nivel de estructura de la aplicación esta tiene dos partes facilmente identificables viendo el código.

  • La libreria que implenta las funcionalidades detalladas por las 3 historias de usuario y sus tests

  • Una pequeña aplicación de consola que haciendo uso de los componentes de la libreria permite jugar simulando las acciones descritas por cada US

Todo el código se encuentra dentro de src:

  • src/Game contiene la classe que forma el core de la aplicación de consola

  • src/Lib contiene las 4 classes que forman el core del backend, de la libreria y que se ajustan a cada US y sus correspondientes UAT

Desarrollo de la Kata

Los test de aceptación de cada historia de usuario guían la realización de esta kata. Las historias de usuario indican lo que se espera en cada fase y queda claro que se trata de implementar solo las funcionalidades requeridas por ellas. Ni una más ni una menos para tener en verde todos los tests.

Para centrarme en la Feature 1 de la kata original he tratado cada Historia de usuario como una Feature en Behat. Este es el detalle de cada una de ellas:

US 1 - Token Can Move Across the Board

Todo empieza con añadir en el fichero us1-move-across-board.feature toda la descripción en Gherkin de esta primera US.

Feature: US 1 - Token Can Move Across the Board
  As a player
  I want to be able to move my token
  So that I can get closer to the goal

  Scenario: UAT1 Start the game
    Given the game is started
    When the token is placed on the board
    Then the token is on square 1

  Scenario: UAT2 Token on square 1
    Given the token is on square 1
    When the token is moved 3 spaces
    Then the token is on square 4

  Scenario: UAT3 Token on square 8
    Given the token is on square 1
    When the token is moved 3 spaces
    And then it is moved 4 spaces
    Then the token is on square 8

A partir de aquí Behat con:

$ behat/bin --append-snippets

Se generan automáticamente los métodos dentro de bootstrap/FeatureContext.php que corresponden a cada linea Given/When/And/Then de cada UAT. Los metodos estan vacios, solo actuan como punto de entrada, y se trata de ir uno por uno aplicando el código necesario para pasar el test.

El proceso es ejecutar bin/behat ver todos los test en rojo, proceder a resolver UAT por UAT implementando el mínimo código para pasar el test, tener test en verde y refactorizar.

Estare repitiendo este ciclo durante las 3 user stories, y creando 3 ficheros .feature con las user stories y los test de aceptación, para que Behat los procese para ir ejecuntando los tests.

En esta primera US veo necesario tener ya la class Game que da sentido a Given the game is started y será el punto de inicio de cualquier partida. Aparece también la class Token con la que moverse por el tablero.

US 2 - Player Can Win the Game

Aquí la cosa ya se pone más interesante, Player cobra más importancia en los test de aceptación de esta user story, por lo que decido crear una class Player que es la que mantiene el estado del jugador y a su vez lo mueve por el tablero mediante Token Esto implica también refactorizar Game para que haga una instancia de Player en vez de Token A partir de este momento el juego arranca con un Player que a su vez dispone de su propio Token

Game adquiere también más importancia concentro en ella las reglas del juego, el check de si el jugador gana o no.

Esta combinación de Game/Player/Token permite resolver los 2 test de aceptación y a la vez mantener responsabilidades separadas, mientras el resto de tests de la user story 1 se mantienen también en verde.

US 3 - Moves Are Determined By Dice Rolls

En este paso creo una nueva class Dice. Separo de esta manera la responsabilidad de generar una tirada de dados. Player en este momento es la class que asume el control de Token y de Dice

Sigo usando Asserts de PHPUnit para controlar resultados concretos dentro de un método que responde a una acción de un test de aceptación, por ejemplo si el valor de los dados está dentro de un rango calculado:

/**
 * @Then the result should be between :arg1-:arg2 inclusive
 */
public function theResultShouldBeBetweenInclusive($arg1, $arg2)
{
    $sides = range($arg1, $arg2);
    Assert::assertContains($this->diceresult, $sides);
}

O para confirmar que realmente el movimiento del token ha correspondido con el número de pasos indicados por el dado:

/**
 * @Then the token should move :arg1 spaces
 */
public function theTokenShouldMoveSpaces($arg1)
{
    $old = $this->player->getOldPosition();
    $new = $this->player->getPosition();

    $rslt = $new-$old;

    Assert::assertEquals($arg1, $rslt);
    
}

Finalizando esta tercera user story, todos los test de aceptación de cada una de ellas pasan en verde.

Desarrollo de la aplicación de consola

La aplicación de consola actua como un frontend para poner a prueba la libreria. Para su desarrollo he usado el componente Console del Symfony Framework, que permite disponer de los elementos basicos para crear una aplicación de consola, gestiónar input via parámetros o teclado y gestionar su output.

En src/Game/GameCommand.php se encuentra el core de la aplicación de consola. Es una class que hereda del la class Command de Symfony y sobreescribe dos metodos: configure donde especificamos los parámetros que aceptara la aplicación, instrucciones, etc. y execute que es el metodo encargado de su funcionamiento.

Esta class esta ya haciendo uso de la libreria con el core del juego.

use SnakesAndLadders\Lib\Game;

Con este componente ya puede iniciar el juego, el jugador, moverlo y lanzar dados.

Este enfoque modular permite separar el backend del frontend y por otra parte el código queda más desacoplado, con responsabilidades muy concretas para cada componente (class) lo que facilita los test y el mantenimiento.

Ha sido mi primera vez con Behat y un proceso muy básico de BDD pero me ha gustado esta kata porque obliga a desarrollar unas funcionalidades sin salirse de lo que se pide en las historias de usuario y generando test de aceptación que dejan cubiertas todas la peticiones de negocio.

Un apunte sobre los tests

Usando Behat el código de los test va todo en bootstrap/FeatureContext.php, localizar las sentencias Give/When/Then/And debe hacerse mirando los comentarios. Behat usa PHPDOC para indicar cada sentencia y su parametrización.

Luego genera nombres de metodos de acuerdo a la sentencia correspondiente.

Behat se apoya en los comentarios para identificar y controlar el comportamiento de cada sentencia.

En la salida de los tests tambien indica el nombre del metodo que resuelva cada sentencia de un escenario.

Tambien puede configurase Behat para usar contextos diferentes y de esta manera no queda todo en un solo bootstrap/FeatureContext.php sino que se puede repartir en varios ficheros lo que permitiria, por ejemplo, tener historias de usuario en contextos diferentes o tipos de test diferentes para diferentes contextos. Pero esta es mi primera vez con Behat y no he querido complicarme a este punto por eso estan todos en bootstrap/FeatureContext.php

Ejecutar el proyecto

Si quieres probarlo una opción es usar el docker que he configurado que lo incluye todo. Este Dockerfile monta un contenedor con PHP 7.4, composer, vim, clona el proyecto con sus dependencias y lo deja listo para lanzar los test y evaluar su funcionamiento. Sin implicaciones en el sistema anfitrión más que disponer de git y Docker instalado.

Si no tienes docker en tu sistema lo puedes instalar con estas instrucciones

Ejecutar el Docker y arrancar el entorno

En tu terminal clona este repositorio y luego muevete dentro del directorio SnakesAndLadders y ejecuta estos comandos de Docker.

$ docker-compose build --no-cache && docker-compose up -d --force-recreate

Esto inicializara el contenedor, puede tardar unos minutos, luego ejecuta:

$ docker-compose run php /bin/bash

Esto te abre una sesión SSH con el contenedor y te situa en el path donde esta clonado todo el proyecto con sus dependencias instaladas, verás algo así:

root@3679266af703:/usr/local/src#

Puedes listar los contenidos de este directorio para confirmar:

root@3679266af703:/usr/local/src# ls -la

Verás algo así:

drwxr-xr-x 1 root root   4096 Jul  4 00:35 .
drwxr-xr-x 1 root root   4096 Jun 23 08:43 ..
drwxr-xr-x 8 root root   4096 Jul  4 00:35 .git
-rw-r--r-- 1 root root     25 Jul  4 00:35 .gitignore
-rw-r--r-- 1 root root    387 Jul  4 00:35 Dockerfile
-rw-r--r-- 1 root root  26294 Jul  4 00:35 README.md
-rw-r--r-- 1 root root    635 Jul  4 00:35 composer.json
-rw-r--r-- 1 root root 139382 Jul  4 00:35 composer.lock
-rw-r--r-- 1 root root    120 Jul  4 00:35 docker-compose.yml
drwxr-xr-x 3 root root   4096 Jul  4 00:35 features
-rw-r--r-- 1 root root    345 Jul  4 00:35 game.php
-rw-r--r-- 1 root root    988 Jul  4 00:35 phpunit.xml
drwxr-xr-x 4 root root   4096 Jul  4 00:35 src

Instalar las dependencias

Aquí debes ejecutar:

root@3679266af703:/usr/local/src# composer update

Esto instalara todas las dependencias necesarias para ejecutar el juego y los tests. Cuando finalize puedes lanzar los test para confirmar que esta todo ok.

Lanzar los tests

Para lanzar los tests y ver los resultados con bin/behat:

root@3679266af703:/usr/local/src# bin/behat

Esto es lo que te mostrara:

Feature: US 1 - Token Can Move Across the Board
  As a player
  I want to be able to move my token
  So that I can get closer to the goal

  Scenario: UAT1 Start the game           # features/us1-move-across-board.feature:6
    Given the game is started             # FeatureContext::theGameIsStarted()
    When the token is placed on the board # FeatureContext::theTokenIsPlacedOnTheBoard()
    Then the token is on square 1         # FeatureContext::theTokenIsOnSquare()

  Scenario: UAT2 Token on square 1   # features/us1-move-across-board.feature:11
    Given the token is on square 1   # FeatureContext::theTokenIsOnSquare()
    When the token is moved 3 spaces # FeatureContext::theTokenIsMovedSpaces()
    Then the token is on square 4    # FeatureContext::theTokenIsOnSquare()

  Scenario: UAT3 Token on square 8   # features/us1-move-across-board.feature:16
    Given the token is on square 1   # FeatureContext::theTokenIsOnSquare()
    When the token is moved 3 spaces # FeatureContext::theTokenIsMovedSpaces()
    And then it is moved 4 spaces    # FeatureContext::thenItIsMovedSpaces()
    Then the token is on square 8    # FeatureContext::theTokenIsOnSquare()

Feature: US 2 - Player Can Win the Game
  As a player
  I want to be able to win the game
  So that I can gloat to everyone around

  Scenario: UAT1 Won the game        # features/us2-player-can-win-game.feature:6
    Given the token is on square 97  # FeatureContext::theTokenIsOnSquare()
    When the token is moved 3 spaces # FeatureContext::theTokenIsMovedSpaces()
    Then the token is on square 100  # FeatureContext::theTokenIsOnSquare()
    And the player has won the game  # FeatureContext::thePlayerHasWonTheGame()

  Scenario: UAT2 Not won the game       # features/us2-player-can-win-game.feature:12
    Given the token is on square 97     # FeatureContext::theTokenIsOnSquare()
    When the token is moved 4 spaces    # FeatureContext::theTokenIsMovedSpaces()
    Then the token is on square 97      # FeatureContext::theTokenIsOnSquare()
    And the player has not won the game # FeatureContext::thePlayerHasNotWonTheGame()

Feature: US 3 - Moves Are Determined By Dice Rolls
  As a player
  I want to move my token based on the roll of a die
  So that there is an element of chance in the game

  Scenario: UAT1 Dice result should be between 1-6 inclusive # features/us3-moves-determined-by-dice.feature:6
    Given the game is started                                # FeatureContext::theGameIsStarted()
    When the player rolls a die                              # FeatureContext::thePlayerRollsA()
    Then the result should be between 1-6 inclusive          # FeatureContext::theResultShouldBeBetweenInclusive()

  Scenario: UAT2 Player rolls a 4       # features/us3-moves-determined-by-dice.feature:11
    Given the player rolls a 4          # FeatureContext::thePlayerRollsA()
    When they move their token          # FeatureContext::theyMoveTheirToken()
    Then the token should move 4 spaces # FeatureContext::theTokenShouldMoveSpaces()

7 scenarios (7 passed)
24 steps (24 passed)
0m0.12s (9.36Mb)

Indicando que la Libreria desarrollada pasa todo los test de cada US.

Ya tienes el entorno en funcionamiento y has podido comprobar que todos los tests estan en verde! 👏

Ejecutar la aplicación

En la raiz del proyecto tienes game.php que es el punto de entrada a la aplicación de consola, se ejecuta como un script php.

El juego ahora funciona al completo, y solo. Cuando ejecutes el comando php game.php empezara y continuara realizando lanzamientos de dados y movimiento del jugador hasta hacerlo ganar.

root@3679266af703:/usr/local/src# php game.php 

El resultado sera algo parecido a esto:

Dice show: 6
Player move token 6 squares
Player at square: 99
Player at snake square, moved to new position 80

Dice show: 6
Player move token 6 squares
Player at square: 86

Dice show: 4
Player move token 4 squares
Player at square: 90

Dice show: 1
Player move token 1 squares
Player at square: 91

Dice show: 6
Player move token 6 squares
Player at square: 97

Dice show: 6
Player can't move
Player at square: 97

Dice show: 4
Player can't move
Player at square: 97

Dice show: 6
Player can't move
Player at square: 97

Dice show: 5
Player can't move
Player at square: 97

Dice show: 3
Player move token 3 squares
Player at square: 100
Player WIN!!!!

Tambien tienes el parametro --bysteps. Usando este parametro el juego, por cada lanzamiento de dados y movimiento, te preguntara si deseas continuar.

Pulsado la tecla Y + [intro] continuas, y pulsando la tecla N +[intro] el juego termina en ese punto. Esto te permite ver paso a paso com va jugando.

root@3679266af703:/usr/local/src# php game.php --bysteps

Player at square: 1

Dice show: 5
Player move token 5 squares
Player at square: 6
Roll Dice ? [y/n] y

La aplicación ahora controla si el token del jugador cae en una de las casillas de Serpientes o Escaleras:

Dice show: 6
Player move token 6 squares
Player at square: 99
Player at snake square, moved to new position 80

About

Un ejemplo de BDD con PHP y Behat

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages