Código heredado de refactorización Parte 5 - Métodos comprobables del juego

Código antiguo. Código feo Código complicado. Código de espagueti. Tonterías gibernéticas. En dos palabras, Código Legado. Esta es una serie que te ayudará a trabajar y lidiar con ella..

En nuestro tutorial anterior, probamos nuestras funciones Runner. En esta lección, es hora de continuar donde lo dejamos probando nuestros Juego clase. Ahora, cuando comienzas con una gran cantidad de código como el que tenemos aquí, es tentador comenzar a probar de arriba hacia abajo, método por método. Esto es, la mayor parte del tiempo, imposible. Es mucho mejor comenzar a probarlo por sus métodos cortos y comprobables. Esto es lo que haremos en esta lección: encuentre y pruebe esos métodos.

Creando un juego

Para probar una clase necesitamos inicializar un objeto de ese tipo específico. Podemos considerar que nuestra primera prueba es crear un nuevo objeto. Te sorprenderás de cuántos secretos pueden ocultar los constructores..

require_once __DIR__. '/… /Trivia/php/Game.php'; la clase GameTest extiende PHPUnit_Framework_TestCase function testWeCanCreateAGame () $ game = new Game (); 

Para nuestra sorpresa, Juego En realidad se puede crear muy fácilmente. No hay problemas mientras se ejecuta solo nuevo juego(). Nada se rompe. Este es un muy buen comienzo, especialmente considerando que JuegoEl constructor es bastante grande y hace muchas cosas..

Encontrar el primer método comprobable

Es tentador simplificar el constructor ahora mismo. Pero solo tenemos el maestro de oro para asegurarnos de no romper nada. Antes de ir al constructor, tenemos que probar la mayor parte del resto de la clase. Entonces, ¿dónde deberíamos empezar??

Busque el primer método que devuelva un valor y pregúntese: "¿Puedo llamar y controlar el valor de retorno de este método?". Si la respuesta es afirmativa, es un buen candidato para nuestra prueba..

function isPlayable () $ minimumNumberOfPlayers = 2; return ($ this-> howManyPlayers ()> = $ minimumNumberOfPlayers); 

¿Qué pasa con este método? Parece un buen candidato. Sólo dos líneas y devuelve un valor booleano. Pero espera, llama a otro método., cuantos jugadores().

function howManyPlayers () return count ($ this-> players); 

Esto es básicamente un método que cuenta los elementos en la clase ' jugadores formación. De acuerdo, si no agregamos jugadores, debería ser cero.. isplayable () debe devolver falso. A ver si nuestra suposición es correcta..

function testAJustCreatedNewGameIsNotPlayable () $ game = new Game (); $ this-> assertFalse ($ game-> isPlayable ()); 

Cambiamos el nombre de nuestro método de prueba anterior para reflejar lo que realmente queremos probar. Entonces simplemente afirmamos que el juego no es jugable. La prueba pasa. Pero los falsos positivos son comunes en muchos casos. Por lo tanto, podemos estar seguros de que la prueba falla.

$ this-> assertTrue ($ game-> isPlayable ());

Y lo hace!

PHPUnit_Framework_ExpectationFailedException: Falló al afirmar que false es true.

Hasta ahora, bastante prometedor. Conseguimos probar el valor de retorno inicial del método, el valor representado por el inicial estado del Juego clase. Por favor, tenga en cuenta la palabra subrayada: "estado". Necesitamos encontrar una manera de controlar el estado del juego. Necesitamos cambiarlo, para que tenga el número mínimo de jugadores..

Si analizamos Juegoes añadir() Método, veremos que agrega elementos a nuestra matriz..

array_push ($ this-> players, $ playerName);

Nuestro supuesto se hace cumplir por la forma en que añadir() método se utiliza en RunnerFunctions.php.

function run () $ aGame = new Game (); $ aGame-> add ("Chet"); $ aGame-> add ("Pat"); $ aGame-> add ("Sue"); //… //

Basándonos en estas observaciones, podemos concluir que al usar añadir() dos veces, deberíamos poder traer nuestra Juego en un estado con dos jugadores.

function testAfterAddingTwoPlayersToANewGameItIsPlayable () $ game = new Game (); $ game-> add ('First Player'); $ game-> add ('Second Player'); $ this-> assertTrue ($ game-> isPlayable ()); 

Al agregar este segundo método de prueba, podemos asegurar isplayable () devuelve true, si se cumplen las condiciones.

Pero puedes pensar que esto no es una prueba de unidad. Usamos el añadir() ¡método! Ejercemos más que el mínimo de código. Podríamos en cambio simplemente agregar los elementos a la $ jugadores matriz y no confiar en el añadir() método en absoluto.

Bueno, la respuesta es si o no. Podríamos hacerlo, desde un punto de vista técnico. Tendrá la ventaja del control directo sobre la matriz. Sin embargo, tendrá la desventaja de la duplicación de código entre el código y las pruebas. Por lo tanto, elija una de las malas opciones con las que cree que puede vivir y use esa. Personalmente prefiero reutilizar métodos como añadir().

Pruebas de refactorización

Estamos en verde, nos refactorizamos. ¿Podemos mejorar nuestras pruebas? Pues sí, podemos. Podríamos transformar nuestra primera prueba para verificar todas las condiciones de jugadores insuficientes.

function testAGameWithNotEnoughPlayersIsNotPlayable () $ game = new Game (); $ this-> assertFalse ($ game-> isPlayable ()); $ game-> add ('Un jugador'); $ this-> assertFalse ($ game-> isPlayable ()); 

Es posible que haya escuchado sobre el concepto de "Una aserción por prueba". En general estoy de acuerdo con eso, pero si tiene una prueba que verifique un concepto único y requiera múltiples aseveraciones para realizar su verificación, creo que es aceptable usar más de una aserción. Esta visión también es fuertemente promovida por Robert C. Martin en sus enseñanzas..

Pero ¿qué pasa con nuestro segundo método de prueba? ¿Es eso lo suficientemente bueno? Yo digo que no.

$ game-> add ('First Player'); $ game-> add ('Second Player');

Estas dos llamadas me molestan un poco. Son una implementación detallada sin una explicación explícita en nuestro método. ¿Por qué no extraerlos en un método privado??

function testAfterAddingEnoughPlayersToANewGameItIsPlayable () $ game = new Game (); $ this-> addEnoughPlayers ($ game); $ this-> assertTrue ($ game-> isPlayable ());  función privada addEnoughPlayers ($ game) $ game-> add ('First Player'); $ game-> add ('Second Player'); 

Esto es mucho mejor y también nos lleva a otro concepto que nos perdimos. En ambas pruebas, expresamos de una forma u otra el concepto de "suficientes jugadores". pero cuanto es suficiente? Son dos? Sí, por ahora lo es. Pero, ¿queremos que nuestra prueba falle si el Juego¿La lógica requerirá al menos tres jugadores? No queremos que esto suceda. Podemos introducir un campo de clase estática pública para ello..

juego de clase static $ minimumNumberOfPlayers = 2; //… // function __construct () //… // function isPlayable () return ($ this-> howManyPlayers ()> = self :: $ minimumNumberOfPlayers);  //… //

Esto nos permitirá utilizarlo en nuestras pruebas..

Función privada addEnoughPlayers ($ game) for ($ i = 0; $ i < Game::$minimumNumberOfPlayers; $i++)  $game->añadir ('Un jugador'); 

Nuestro pequeño método de ayuda solo agregará jugadores hasta que se agregue lo suficiente. Incluso podemos crear otro método similar para nuestra primera prueba, por lo que agregamos casi suficientes jugadores..

function testAGameWithNotEnoughPlayersIsNotPlayable () $ game = new Game (); $ this-> assertFalse ($ game-> isPlayable ()); $ this-> addJustNothEnoughPlayers ($ game); $ this-> assertFalse ($ game-> isPlayable ());  función privada addJustNothEnoughPlayers ($ game) for ($ i = 0; $ i < Game::$minimumNumberOfPlayers - 1; $i++)  $game->agregar ('Un jugador'); 

Pero esto introdujo alguna duplicación. Nuestros dos métodos de ayuda son bastante similares. ¿No podemos extraer un tercero de ellos??

función privada addEnoughPlayers ($ game) $ this-> addManyPlayers ($ game, Game :: $ minimumNumberOfPlayers);  función privada addJustNothEnoughPlayers ($ game) $ this-> addManyPlayers ($ game, Game :: $ minimumNumberOfPlayers - 1);  función privada addManyPlayers ($ game, $ numberOfPlayers) for ($ i = 0; $ i < $numberOfPlayers; $i++)  $game->añadir ('Un jugador'); 

Eso es mejor, pero introduce un problema diferente. Reducimos la duplicación en estos métodos, pero nuestra $ juego objeto ahora se pasa a tres niveles. Se está haciendo difícil de manejar. Es hora de inicializarlo en la prueba. preparar() Método y reutilizarlo..

la clase GameTest extiende PHPUnit_Framework_TestCase private $ game; función setUp () $ this-> game = new Game;  function testAGameWithNotEnoughPlayersIsNotPlayable () $ this-> assertFalse ($ this-> game-> es Playable ()); $ this-> addJustNothEnoughPlayers (); $ this-> assertFalse ($ this-> game-> isPlayable ());  function testAfterAddingEnoughPlayersToANewGameItIsPlayable () $ this-> addEnoughPlayers ($ this-> game); $ this-> assertTrue ($ this-> game-> isPlayable ());  función privada addEnoughPlayers () $ this-> addManyPlayers (Game :: $ minimumNumberOfPlayers);  función privada addJustNothEnoughPlayers () $ this-> addManyPlayers (Game :: $ minimumNumberOfPlayers - 1);  función privada addManyPlayers ($ numberOfPlayers) para ($ i = 0; $ i < $numberOfPlayers; $i++)  $this->game-> add ('A Player'); 

Mucho mejor. Todo el código irrelevante está en métodos privados., $ juego se inicializa en preparar() y se eliminó una gran cantidad de contaminación de los métodos de prueba. Sin embargo, tuvimos que hacer un compromiso aquí. En nuestra primera prueba, comenzamos con una afirmación. Esto supone que preparar() siempre creará un juego vacío. Esto está bien por ahora. Pero al final del día, debes darte cuenta de que no existe el código perfecto. Solo hay un código con compromisos con los que estás dispuesto a vivir..

El segundo método comprobable

Si estamos escaneando nuestro Juego clase de arriba hacia abajo, el siguiente método en nuestra lista es añadir(). Sí, el mismo método que usamos en nuestras pruebas en el párrafo anterior. Pero podemos probarlo?

function testItCanAddANewPlayer () $ this-> game-> add ('A player'); $ this-> assertEquals (1, count ($ this-> game-> players)); 

Ahora esta es una forma diferente de probar objetos. Llamamos a nuestro método y luego verificamos el estado del objeto. Como añadir() siempre vuelve cierto, No hay manera de que podamos probar su salida. Pero podemos empezar con un vacío. Juego objeto y luego verifique si hay un solo usuario después de agregar uno. Pero ¿es eso suficiente verificación??

function testItCanAddANewPlayer () $ this-> assertEquals (0, count ($ this-> game-> players)); $ this-> game-> add ('Un jugador'); $ this-> assertEquals (1, count ($ this-> game-> players)); 

¿No sería mejor verificar también si no hay jugadores antes de que llamemos? añadir()? Bueno, puede que sea un poco demasiado aquí, pero como puede ver en el código anterior, podríamos hacerlo. Y siempre que no esté seguro del estado inicial, debe hacer una afirmación al respecto. Esto también lo protege de futuros cambios de código que pueden cambiar el estado inicial de su objeto.

Pero estamos probando todas las cosas que el añadir() método hace? Yo digo que no. Además de agregar un usuario, también establece muchos ajustes para él. También deberíamos comprobar por aquellos.

function testItCanAddANewPlayer () $ this-> assertEquals (0, count ($ this-> game-> players)); $ this-> game-> add ('Un jugador'); $ this-> assertEquals (1, count ($ this-> game-> players)); $ this-> assertEquals (0, $ this-> game-> places [1]); $ this-> assertEquals (0, $ this-> game-> purses [1]); $ this-> assertFalse ($ this-> game-> inPenaltyBox [1]); 

Este es mejor. Verificamos cada acción que la añadir() método hace Esta vez, preferí probar directamente el $ jugadores formación. ¿Por qué? Podríamos haber usado el cuantos jugadores() Método que básicamente hace lo mismo, ¿verdad? Bueno, en este caso consideramos que es más importante describir nuestras afirmaciones por los efectos que la añadir() El método tiene sobre el estado del objeto. Si necesitamos cambiar añadir(), esperaríamos que la prueba que está probando su comportamiento estricto, fallará. He tenido interminables debates con mis colegas en Syneto sobre esto. Especialmente porque este tipo de prueba introduce un fuerte acoplamiento entre la prueba y cómo la añadir() El método está realmente implementado. Entonces, si prefiere probarlo al revés, eso no significa que sus ideas estén equivocadas..

Podemos ignorar con seguridad la prueba de la salida, la Echoln () líneas. Solo están emitiendo contenido en la pantalla. Aún no queremos tocar estos métodos. Nuestro maestro de oro se apoya totalmente en esta salida..

Pruebas de refactorización (bis)

Tenemos otro método probado con una nueva prueba de aprobación. Es hora de refactorizar a ambos, solo un poco. Empecemos con nuestras pruebas. ¿No son las tres últimas afirmaciones un poco confusas? No parecen estar relacionados estrictamente con agregar un jugador. Vamos a cambiarlo:

function testItCanAddANewPlayer () $ this-> assertEquals (0, count ($ this-> game-> players)); $ this-> game-> add ('Un jugador'); $ this-> assertEquals (1, count ($ this-> game-> players)); $ this-> assertDefaultPlayerParametersAreSetFor (1); 

Eso es mejor. El método ahora es más abstracto, reutilizable, con un nombre expresivo y oculta todos los detalles sin importancia.

Refactorizando el añadir() Método

Podemos hacer algo similar con nuestro código de producción..

función add ($ playerName) array_push ($ this-> players, $ playerName); $ this-> setDefaultPlayerParametersFor ($ this-> howManyPlayers ()); echoln ($ playerName. "fue agregado"); echoln ("Son número de jugador". count ($ this-> players)); devuelve verdadero 

Hemos extraído los detalles sin importancia en setDefaultPlayerParametersFor ().

función privada setDefaultPlayerParametersFor ($ playerId) $ this-> places [$ playerId] = 0; $ this-> purses [$ playerId] = 0; $ this-> inPenaltyBox [$ playerId] = false; 

En realidad, esta idea me vino después de que escribí la prueba. Este es otro buen ejemplo de cómo las pruebas nos obligan a pensar sobre nuestro código desde un punto de vista diferente. Este ángulo diferente del problema es lo que debemos explotar y dejar que nuestras pruebas guíen nuestro diseño del código de producción..

El tercer método comprobable

Encontremos a nuestro tercer candidato para la prueba.. cuantos jugadores() Es demasiado simple e indirectamente ya probado. rodar() Es demasiado complejo para ser probado directamente. Además vuelve nulo. hacer preguntas() Parece ser interesante a primera vista, pero es todo presentación, sin valor de retorno.

currentCategory () es comprobable, pero es bonito difícil Probar. Es un selector enorme con diez condiciones. Necesitamos una prueba de diez líneas y luego debemos refactorizar seriamente este método y, ciertamente, también las pruebas. Debemos tomar nota de este método y volver a él después de que hayamos terminado con los más fáciles. Para nosotros, esto estará en nuestro próximo tutorial..

fue respondido correctamente () Es complicado de nuevo. Tendremos que extraer de él, pequeños trozos de código que sean comprobables. sin embargo, respuesta incorrecta() Parece prometedor. Da salida a cosas en la pantalla, pero también cambia el estado de nuestro objeto. A ver si podemos controlarlo y probarlo..

test de funciónWhenAPlayerEntersAWrongAnswerItIsIsentToThePenaltyBox () $ this-> game-> add ('A player'); $ this-> game-> currentPlayer = 0; $ this-> game-> wrongAnswer (); $ this-> assertTrue ($ this-> game-> inPenaltyBox [0]); 

Grrr ... fue bastante difícil escribir este método de prueba. respuesta incorrecta() se basa en $ this-> currentPlayer Por su lógica de comportamiento, pero también utiliza. $ this-> jugadores En su parte de presentación. Un ejemplo feo de por qué no debes mezclar lógica y presentación. Nos ocuparemos de esto en un futuro tutorial. Por ahora, comprobamos que el usuario ingresa al cuadro de penalización. También debemos observar que hay una Si() Declaración en el método. Esta es una condición que aún no probamos, ya que solo tenemos un solo jugador y, por lo tanto, no estamos satisfaciendo la condición. Podríamos probar el valor final de $ currentPlayer aunque. Pero agregar esta línea de código a la prueba hará que falle.

$ this-> assertEquals (1, $ this-> game-> currentPlayer);

Una mirada más cercana al método privado. shouldResetCurrentPlayer () revela el problema Si el índice del jugador actual es igual al número de jugadores, se restablecerá a cero. Aaaahhh! En realidad entramos en el Si()!

test de funciónWhenAPlayerEntersAWrongAnswerItIsIsentToThePenaltyBox () $ this-> game-> add ('A player'); $ this-> game-> currentPlayer = 0; $ this-> game-> wrongAnswer (); $ this-> assertTrue ($ this-> game-> inPenaltyBox [0]); $ this-> assertEquals (0, $ this-> game-> currentPlayer);  function testCurrentPlayerIsNotResetAfterWrongAnswerIfOtherPlayersDidNotYetPlay () $ this-> addManyPlayers (2); $ this-> game-> currentPlayer = 0; $ this-> game-> wrongAnswer (); $ this-> assertEquals (1, $ this-> game-> currentPlayer); 

Bueno. Creamos una segunda prueba, para probar el caso específico cuando todavía hay jugadores que no jugaron. No nos importa el inPenaltyBox Estado para la segunda prueba. Solo nos interesa el índice del jugador actual..

El método comprobable final

El último método que podemos probar y luego refactorizar es didPlayerWin ().

function didPlayerWin () $ numberOfCoinsToWin = 6; return! ($ this-> purses [$ this-> currentPlayer] == $ numberOfCoinsToWin); 

Podemos observar inmediatamente que su estructura de código es muy similar a isplayable (), El método que probamos primero. Nuestra solución debería ser algo similar también. Cuando su código es tan corto, solo dos o tres líneas, no es un gran riesgo hacer más de un pequeño paso. En el peor de los casos, revierte tres líneas de código. Así que vamos a hacer esto en un solo paso.

function testTestPlayerWinsWithTheCorrectNumberOfCoins () $ this-> game-> currentPlayer = 0; $ this-> game-> purses [0] = Game :: $ numberOfCoinsToWin; $ this-> assertTrue ($ this-> game-> didPlayerWin ()); 

¡Pero espera! Que falla ¿Cómo es eso posible? ¿No debería pasar? Proporcionamos el número correcto de monedas. Si estudiamos nuestro método, descubriremos un pequeño dato engañoso..

return! ($ this-> purses [$ this-> currentPlayer] == $ numberOfCoinsToWin);

El valor de retorno es en realidad negado. Entonces, el método no nos dice si un jugador ganó, sino que nos dice si un jugador no ganó el juego. Podríamos entrar y encontrar los lugares donde se usa este método y negar su valor allí. Luego cambia su comportamiento aquí, para no negar falsamente la respuesta. Pero se usa en fue respondido correctamente (), Un método que aún no podemos probar de unidad. Tal vez por el momento, un simple cambio de nombre para resaltar la funcionalidad correcta será suficiente.

function didPlayerNotWin () return! ($ this-> purses [$ this-> currentPlayer] == self :: $ numberOfCoinsToWin); 

Pensamientos y Conclusión

Así que esto trata sobre el tutorial. Si bien no nos gusta la negación en el nombre, este es un compromiso que podemos hacer en este punto. Este nombre seguramente cambiará cuando comencemos a refactorizar otras partes del código. Además, si echas un vistazo a nuestras pruebas, ahora se ven extrañas:

function testTestPlayerWinsWithTheCorrectNumberOfCoins () $ this-> game-> currentPlayer = 0; $ this-> game-> purses [0] = Game :: $ numberOfCoinsToWin; $ this-> assertFalse ($ this-> game-> didPlayerNotWin ()); 

Al probar falso en un método negado, ejercitado con un valor que sugiere un resultado verdadero, introdujimos mucha confusión en la legibilidad de nuestros códigos. Pero esto es bueno por ahora, ya que necesitamos detenernos en algún punto, ¿verdad??

En nuestro próximo tutorial, comenzaremos a trabajar en algunos de los métodos más difíciles dentro del Juego clase. Gracias por leer.