Pruebas más fáciles con burla

Es una verdad desafortunada que, si bien el principio básico detrás de las pruebas es bastante simple, la introducción completa de este proceso en su flujo de trabajo de codificación diario es más difícil de lo que podría esperar. La diversa jerga sola puede resultar abrumadora! Afortunadamente, una variedad de herramientas lo respalda y ayuda a hacer que el proceso sea lo más sencillo posible. Mockery, el principal marco de simulacros de objetos para PHP, es una de esas herramientas.!

En este artículo, analizaremos qué es la burla, por qué es útil y cómo integrar Mockery en su flujo de trabajo de prueba..


Burlándose decodificado

Un objeto simulado no es más que un poco de jerga de prueba que se refiere a simular el comportamiento de objetos reales. En términos más simples, a menudo, al realizar pruebas, no querrá ejecutar un método en particular. En su lugar, simplemente debe asegurarse de que se haya llamado, de hecho,.

Tal vez un ejemplo está en orden. Imagine que su código activa un método que registrará un poco de datos en un archivo. Al probar esta lógica, no querrá tocar físicamente el sistema de archivos. Esto tiene el potencial de disminuir drásticamente la velocidad de sus pruebas. En estas situaciones, es mejor burlarse de su clase de sistema de archivos y, en lugar de leer manualmente el archivo para probar que se actualizó, simplemente asegúrese de que el método aplicable en la clase se haya llamado. Esto es burlarse! No hay nada más que eso; Simular el comportamiento de los objetos..

Recuerda: la jerga es solo una jerga. Nunca permitas que una parte confusa de terminología te impida aprender una nueva habilidad.

Particularmente a medida que su proceso de desarrollo madura, incluyendo el principio de responsabilidad única y el aprovechamiento de la inyección de dependencia, la familiaridad con la burla se volverá rápidamente esencial..

Mocks vs. Stubs: Hay muchas posibilidades de que a menudo oigas los términos, burlarse de y talón, arrojados de manera intercambiable. De hecho, los dos sirven propósitos diferentes. El primero se refiere al proceso de definir expectativas y asegurar el comportamiento deseado. En otras palabras, un simulacro puede conducir potencialmente a una prueba fallida. Un código auxiliar, por otro lado, es simplemente un conjunto de datos ficticios que pueden transmitirse para cumplir ciertos criterios.

La biblioteca de pruebas de facto para PHP, PHPUnit, se entrega con su propia API para simular objetos; sin embargo, desafortunadamente, puede resultar complicado trabajar con él. Como seguramente sabe, cuanto más difíciles son las pruebas, más probable es que el desarrollador simplemente no lo haga..

Afortunadamente, una variedad de soluciones de terceros están disponibles a través de Packagist (repositorio de paquetes de Composer), que permiten una mayor legibilidad y, lo que es más importante,, capacidad de escritura. Entre estas soluciones, y la más notable del conjunto, se encuentra Mockery, un marco simulado de objeto simulado al marco..

Diseñado como una alternativa para aquellos que están abrumados por la verbosidad burlona de PHPUnit, Mockery es una utilidad simple, pero poderosa. Como seguramente encontrará, de hecho, es el estándar de la industria para el desarrollo de PHP moderno..


Instalación

Como la mayoría de las herramientas PHP modernas, Mockery puede instalarse con Composer.

Como la mayoría de las herramientas de PHP en estos días, el método recomendado para instalar Mockery es a través de Composer (aunque también está disponible a través de Pear).

Espera, ¿qué es esto del compositor? Es la herramienta preferida de la comunidad de PHP para la gestión de dependencias. Proporciona una manera fácil de declarar las dependencias de un proyecto y jalarlas con un solo comando. Como desarrollador de PHP moderno, es vital que tengas un conocimiento básico de qué es Composer y cómo usarlo.

Si trabaja a lo largo, para fines de aprendizaje, agregue compositor.json archivar en un proyecto vacío y anexar:

 "require-dev": "mockery / mockery": "dev-master"

Este bit de JSON especifica que, para el desarrollo, su aplicación requiere la biblioteca Mockery. Desde la línea de comandos, una instalación del compositor --dev tirará en el paquete.

$ composer install --dev Carga de repositorios del compositor con información del paquete Instalación de dependencias (incluido require-dev) - Instalación de mockery / mockery (dev-master 5a71299) Clonación 5a712994e1e3ee604b0d355d1af342172c6f475f Escritura de archivo de bloqueo Generación de archivos de carga automática

Como un bono adicional, ¡Composer se envía con su propio autocargador gratis! Especifique un mapa de clase de directorios y compositor dump-autoload, o siga el estándar PSR-0 y ajuste la estructura de su directorio para que coincida. Consulte Nettuts + para obtener más información. Si aún estás requiriendo manualmente innumerables archivos en cada archivo PHP, bueno, es posible que lo estés haciendo mal..


El dilema

Antes de que podamos implementar una solución, es mejor revisar primero el problema. Imagine que necesita implementar un sistema para manejar el proceso de generar contenido y escribirlo en un archivo. Tal vez el generador compile varios datos, ya sea de apéndices de archivos locales o un servicio web, y luego esos datos se escriben en el sistema de archivos.

Si se sigue el principio de responsabilidad única. - que dicta que cada clase debe ser responsable de exactamente una cosa - entonces es lógico pensar que deberíamos dividir esta lógica en dos clases: una para generar el contenido necesario y otra para escribir físicamente los datos en un archivo. UNA Generador y Expediente clase, respectivamente, debe hacer el truco.

Propina: ¿Por qué no usar file_put_contents directamente desde el Generador ¿clase? Bueno, pregúntate: "¿Cómo podría probar esto??"Existen técnicas, como el parcheo de monos, que pueden permitirle sobrecargar este tipo de cosas, pero, como práctica recomendada, es mejor envolver esa funcionalidad, de modo que se pueda burlar fácilmente con herramientas, como Mockery!

Aquí hay una estructura básica (con una buena dosis de pseudo código) para nuestra Generador clase.

archivo = $ archivo;  función protegida getContent () // simplificada para la demostración demo 'foo bar';  función pública fire () $ content = $ this-> getContent (); $ this-> file-> put ('foo.txt', $ content); 

Inyección de dependencia

Este código aprovecha lo que llamamos inyección de dependencia. Una vez más, esto es simplemente una jerga de desarrolladores para inyectar las dependencias de una clase a través de su método de construcción, en lugar de codificarlas.

¿Por qué es esto beneficioso? Porque, de lo contrario, no podríamos burlarnos del Expediente ¡clase! Claro, podríamos burlarnos del Expediente clase, pero si su instanciación está codificada en la clase que estamos probando, no hay una manera fácil de reemplazar esa instancia con la versión simulada.

función pública __construct () // anti-patrón $ this-> file = new File; 

La mejor manera de crear una aplicación comprobable es acercarse a cada nueva llamada de método con la pregunta "Como podria probar esto?"Si bien existen trucos para sortear esta codificación rígida, se considera que hacerlo es una mala práctica. En su lugar, inyecte las dependencias de una clase a través del constructor, o mediante la inyección del definidor..

La inyección de Setter es más o menos idéntica a la inyección de constructor. El principio es exactamente el mismo; la única diferencia es que, en lugar de inyectar las dependencias de la clase a través de su método constructor, en lugar de eso, lo hacen a través de un método setter, de este modo:

función pública setFile (archivo $ archivo) $ this-> archivo = $ archivo; 

Una crítica común de la inyección de dependencia es que introduce complejidad adicional en una aplicación, todo para hacerla más comprobable. Aunque el argumento de la complejidad es discutible en la opinión de este autor, si lo prefiere, puede permitir la inyección de dependencia, mientras que aún especifica los valores predeterminados de reserva. Aquí hay un ejemplo:

Generador de clase función pública __construcción (archivo $ archivo = nulo) $ esto-> archivo = $ archivo?: archivo nuevo; 

Ahora, si una instancia de Expediente se pasa al constructor, ese objeto se usará en la clase. Por otro lado, si no se pasa nada, el Generador será retroceder para instanciar manualmente la clase aplicable. Esto permite variaciones tales como:

# La clase crea una instancia de File new Generator; # Inyectar nuevo generador (nuevo archivo); # Inyecte un simulacro de archivo para probar el nuevo generador ($ mockedFile);

Continuando, a los efectos de este tutorial, el Expediente La clase no será más que un simple envoltorio alrededor de PHP file_put_contents función.

 

Más bien simple, ¿eh? Vamos a escribir una prueba para ver, de primera mano, cuál es el problema..

fuego(); 

Tenga en cuenta que estos ejemplos asumen que las clases necesarias se están cargando automáticamente con Composer. Tu compositor.json archivo opcionalmente acepta un carga automática objeto, donde puede especificar qué directorios o clases autocargar. No mas desordenado exigir declaraciones!

Si trabajamos a lo largo, corriendo phpunit volverá:

OK (1 test, 0 aserciones)

Es verde; eso significa que podemos pasar a la siguiente tarea, ¿verdad? Bueno no exactamente. Si bien es cierto que el código sí funciona, cada vez que se ejecuta esta prueba, foo.txt archivo se creará en el sistema de archivos. ¿Qué pasa cuando has escrito docenas de pruebas más? Como se puede imaginar, muy rápidamente, la velocidad de ejecución de su prueba tartamudeará.

Aunque las pruebas pasan, están tocando incorrectamente el sistema de archivos..

¿Todavía no está convencido? Si la velocidad reducida de la prueba no lo afecta, entonces considere el sentido común. Piénsalo: estamos probando el Generador clase; ¿Por qué tenemos interés en ejecutar código desde el Expediente ¿clase? ¡Debería tener sus propias pruebas! ¿Por qué diablos nos doblaríamos??


La solución

Con suerte, la sección anterior proporcionó la ilustración perfecta de por qué la burla es esencial. Como se señaló anteriormente, aunque podríamos utilizar la API nativa de PHPUnit para cumplir con nuestros requisitos de burla, no es demasiado placentero trabajar con él. Para ilustrar esta verdad, aquí hay un ejemplo para afirmar que un objeto simulado debe recibir un método, getName y volver John Doe.

función pública testNativeMocks () $ mock = $ this-> getMock ('SomeClass'); $ mock-> expects ($ this-> once ()) -> method ('getName') -> will ($ this-> returnValue ('John Doe')); 

Mientras hace el trabajo - afirmando que un getName Método se llama una vez, y devuelve John Doe - La implementación de PHPUnit es confusa y detallada. Con Mockery, podemos mejorar drásticamente su legibilidad.

función pública testMockery () $ mock = Mockery :: mock ('SomeClass'); $ mock-> shouldEreciben ('getName') -> once () -> andReturn ('John Doe'); 

Observe cómo el último ejemplo lee (y habla) mejor.

Continuando con el ejemplo del anterior "Dilema sección, esta vez, dentro de la GeneratorTest clase, en lugar de eso nos burlamos, o simulamos el comportamiento de Expediente clase con burla. Aquí está el código actualizado:

shouldReceive ('put') -> with ('foo.txt', 'foo bar') -> once (); $ generator = new Generator ($ mockedFile); $ generador-> fuego (); 

Confundido por el Burla :: cerrar () referencia dentro del demoler ¿método? Esta llamada estática limpia el contenedor de Mockery utilizado por la prueba actual y ejecuta las tareas de verificación necesarias para sus expectativas.

Una clase puede ser burlada usando el legible Mockery :: Mock () método. A continuación, normalmente deberá especificar qué métodos de este objeto simulado espera recibir, junto con los argumentos aplicables. Esto se puede lograr, a través de la debería recibir (método) y con (ARG) metodos.

En este caso, cuando llamamos. $ generar-> fuego (), Estamos afirmando que debería llamar al poner método en el Expediente instancia, y envíale el camino, foo.txt, y los datos, foo bar.

// bibliotecas / Generator.php public function fire () $ content = $ this-> getContent (); $ this-> file-> put ('foo.txt', $ content); 

Debido a que estamos usando la inyección de dependencia, ahora es muy fácil inyectar el imitador Expediente objeto.

$ generator = new Generator ($ mockedFile);

Si volvemos a ejecutar las pruebas, seguirán volviendo verde, sin embargo, el Expediente clase - y, en consecuencia, el sistema de archivos - nunca será tocado! Una vez más, no hay necesidad de tocar Expediente. ¡Debería tener sus propias pruebas! Burlándose de la victoria!

Simulacros de objetos simples

Los objetos simulados no siempre tienen que hacer referencia a una clase. Si solo necesita un objeto simple, tal vez para un usuario, puede pasar una matriz a la burlarse de método: donde, para cada elemento, la clave y el valor corresponden al nombre del método y al valor de retorno, respectivamente.

función pública testSimpleMocks () $ user = Mockery :: mock (['getFullName' => 'Jeffrey Way']); $ usuario-> getFullName (); // Jeffrey Way

Valores de retorno de métodos simulados

Seguramente habrá ocasiones en que un método de clase simulado deba devolver un valor. Continuando con nuestro ejemplo de generador / archivo, ¿qué sucede si necesitamos asegurarnos de que, si el archivo ya existe, no se debe sobrescribir? ¿Cómo podríamos lograr eso??

La clave es usar el y volver() Método en su objeto simulado para simular diferentes estados. Aquí hay un ejemplo actualizado:

función pública testDoesNotOverwriteFile () $ mockedFile = Mockery :: mock ('File'); $ mockedFile-> shouldReciba ('existe') -> once () -> yRetorno (verdadero); $ mockedFile-> shouldReceive ('put') -> never (); $ generator = new Generator ($ mockedFile); $ generador-> fuego (); 

Este código actualizado ahora afirma que un existe método debe ser activado en el simulado Expediente clase, y debe, a los efectos de la ruta de esta prueba, devolver cierto, señal de que el archivo ya existe y no debe sobrescribirse. A continuación, nos aseguramos de que, en situaciones como esta, la poner método en el Expediente La clase nunca se activa. Con Mockery, esto es fácil, gracias a la Nunca() expectativa.

$ mockedFile-> shouldReceive ('put') -> never ();

Si volvemos a ejecutar las pruebas, se devolverá un error:

El método existe () desde el archivo se debe llamar exactamente 1 veces pero se llama 0 veces.

Ajá así que la prueba esperaba que $ este-> archivo-> existe () Debería llamarse, pero eso nunca sucedió. Como tal, fracasó. Vamos a arreglarlo!

archivo = $ archivo;  función protegida getContent () // simplificada para la demostración demo 'foo bar';  función pública fire () $ content = $ this-> getContent (); $ archivo = 'foo.txt'; si (! $ this-> file-> existe ($ file)) $ this-> file-> put ($ file, $ content); 

¡Eso es todo al respecto! No solo hemos seguido un ciclo TDD (desarrollo guiado por pruebas), sino que las pruebas han regresado a verde.!

Es importante recordar que este estilo de prueba solo es efectivo si, de hecho, ¡también prueba las dependencias de su clase! De lo contrario, aunque las pruebas pueden mostrarse en verde, para la producción, el código se romperá. Nuestra demostración hasta ahora solo ha asegurado que Generador Funciona como se espera. No te olvides de probar Expediente también!


Esperanzas de heredar

Vamos a profundizar un poco más en las declaraciones de expectativas de Mockery. Ya estas familiarizado con debería recibir. Tenga cuidado con esto, sin embargo; Su nombre es un poco engañoso. Cuando se deja solo, no requiere que el método se active; el valor predeterminado es cero o más veces (zeroOrMoreTimes ()). Para afirmar que necesita que se llame al método una vez, o potencialmente más veces, hay disponibles algunas opciones:

$ mock-> shouldReceive ('method') -> once (); $ mock-> shouldReceive ('method') -> times (1); $ mock-> shouldReceive ('method') -> atLeast () -> times (1);

Habrá momentos en que sean necesarias restricciones adicionales. Como se demostró anteriormente, esto puede ser particularmente útil cuando necesita asegurarse de que un método en particular se active con los argumentos necesarios. Es importante tener en cuenta que la expectativa solo se aplicará si se llama a un método con estos argumentos exactos.

Aquí hay algunos ejemplos..

$ mock-> shouldReceive ('get') -> withAnyArgs () -> once (); // el valor predeterminado $ mock-> shouldReceive ('get') -> with ('foo.txt') -> once (); $ mock-> shouldReceive ('put') -> with ('foo.txt', 'foo bar') -> once ();

Esto se puede extender aún más para permitir que los valores de los argumentos sean de naturaleza dinámica, siempre que cumplan con ciertos criterios. Quizás solo deseamos asegurarnos de que una cadena se pasa a un método:

$ mock-> shouldReceive ('get') -> with (Mockery :: type ('string')) -> once ();

O tal vez el argumento debe coincidir con una expresión regular. Afirmemos que cualquier nombre de archivo que termine con .TXT debe coincidir.

$ mockedFile-> shouldReceive ('put') -> with ('/ \. txt $ /', Mockery :: any ()) -> once ();

Y como ejemplo final (pero no limitado a), vamos a permitir una matriz de valores aceptables, usando el cualquiera de matcher.

$ mockedFile-> shouldReceive ('get') -> with (Mockery :: anyOf ('log.txt', 'cache.txt')) -> once ();

Con este código, la expectativa solo se aplicará si el primer argumento de la obtener método es log.txt o cache.txt. De lo contrario, se lanzará una excepción de Mockery cuando se ejecuten las pruebas..

Mockery \ Exception \ NoMatchingExpectationException: no se encontró un controlador coincidente ... 

Propina: No lo olvides, siempre puedes alias. Burlas como metro en la parte superior de tu clase para hacer las cosas un poco más sucintas: usa la burla como m;. Esto permite que los más sucintos., m :: simulacro ().

Por último, tenemos una variedad de opciones para especificar qué debe hacer o devolver el método simulado. Tal vez solo lo necesitamos para devolver un booleano. Fácil:

$ mock-> shouldReceive ('method') -> once () -> andReturn (false);

Mock parcial

Puede encontrar que hay situaciones en las que solo necesita burlarse de un solo método, en lugar de todo el objeto. Imaginemos, para los fines de este ejemplo, que un método en su clase hace referencia a una función global personalizada (jadeo) para obtener un valor de un archivo de configuración.

getOption ('timeout'); // hacer algo con $ timeout

Si bien hay algunas técnicas diferentes para burlarse de las funciones globales. sin embargo, lo mejor es evitar este método, convocar a todos juntos. Esto es precisamente cuando entran en juego simulacros parciales..

función pública testPartialMockExample () $ mock = Mockery :: mock ('MyClass [getOption]'); $ mock-> shouldReceive ('getOption') -> once () -> andReturn (10000); $ simulacro-> fuego (); 

Observe cómo hemos colocado el método para burlarse entre paréntesis. Si tiene varios métodos, simplemente sepárelos con una coma, así:

$ mock = Mockery :: mock ('MyClass [method1, method2]');

Con esta técnica, el resto de los métodos en el objeto se activarán y se comportarán como lo harían normalmente. Tenga en cuenta que siempre debe declarar el comportamiento de sus métodos simulados, como hemos hecho anteriormente. En este caso, cuando getOption se llama, en lugar de ejecutar el código dentro de él, simplemente regresamos 10000.

Una opción alternativa es utilizar simulacros parciales pasivos, que se pueden considerar como establecer un estado predeterminado para el objeto simulado: todos los métodos se remiten a la clase principal, a menos que se especifique una expectativa.

El fragmento de código anterior se puede reescribir como:

función pública testPassiveMockExample () $ mock = Mockery :: mock ('MyClass') -> makePartial (); $ mock-> shouldReceive ('getOption') -> once () -> andReturn (10000); $ simulacro-> fuego (); 

En este ejemplo, todos los métodos en Mi clase se comportarán como lo harían normalmente, excluyendo getOption, que será burlado y devuelto 10000 '.


Hamcrest

La biblioteca Hamcrest proporciona un conjunto adicional de comparadores para definir expectativas.

Una vez que se haya familiarizado con la API de Mockery, se recomienda que también aproveche la biblioteca Hamcrest, que proporciona un conjunto adicional de comparadores para definir expectativas legibles. Al igual que Mockery, se puede instalar a través de Composer..

"require-dev": "mockery / mockery": "dev-master", "davedevelopment / hamcrest-php": "dev-master"

Una vez instalado, puede usar una notación más legible para definir sus pruebas. A continuación, se incluyen algunos ejemplos, que incluyen pequeñas variaciones que logran el mismo resultado final..

 

Observe cómo Hamcrest le permite escribir sus afirmaciones de la forma más legible o concisa que desee. El uso de la es() La función no es más que azúcar sintáctica para ayudar a la legibilidad..

Encontrarás que Mockery combina bastante bien con Hamcrest. Por ejemplo, solo con Mockery, para especificar que un método simulado debe llamarse con un solo argumento de tipo, cuerda, usted podría escribir:

$ mock-> shouldReceive ('method') -> with (Mockery :: type ('string')) -> once ();

Si usa Hamcrest, Mockery :: type puede ser reemplazado con valor de cadena(), al igual que:

$ mock-> shouldReceive ('method') -> with (stringValue ()) -> once ();

Hamcrest sigue el recursoConvención de nomenclatura de valores para hacer coincidir el tipo de un valor.

  • valor nulo
  • valor entero
  • arrayValue
  • enjuague y repita

Alternativamente, para que coincida con cualquier argumento, Burla :: cualquiera () podría convertirse cualquier cosa().

$ file-> shouldReceive ('put') -> with ('foo.txt', cualquier cosa ()) -> once ();

Resumen

El mayor obstáculo para usar Mockery es, irónicamente, no la API en sí misma..

El mayor obstáculo para usar Mockery es, irónicamente, no la API en sí misma, sino comprender por qué y cuándo usar simulaciones en sus pruebas.

La clave es aprender y respetar el principio de responsabilidad única en su flujo de trabajo de codificación. Acuñado por Bob Martin, el SRP dicta que una clase "Debe haber una, y solo una, razón para cambiar.."En otras palabras, una clase no debería necesitar actualizarse en respuesta a múltiples cambios no relacionados con su aplicación, como la modificación de la lógica empresarial, o cómo se formatea la salida, o cómo pueden persistir los datos. En su forma más simple, simplemente Como un método, una clase debería hacer una cosa..

los Expediente La clase maneja las interacciones del sistema de archivos. UNA MysqlDb El repositorio persiste en los datos. Un Email La clase prepara y envía correos electrónicos. Observe cómo, en ninguno de estos ejemplos estaba la palabra, y, usado.

Una vez que se entiende esto, la prueba se vuelve considerablemente más fácil. La inyección de dependencia se debe usar para todas las operaciones que no caen bajo la clase paraguas. Al realizar pruebas, enfóquese en una clase a la vez y simule todas sus dependencias. No estás interesado en probarlos de todos modos; ellos tienen sus propias pruebas!

Aunque nada le impide usar la implementación de burla nativa de PHPUnit, ¿por qué molestarse cuando la mejor legibilidad de Mockery es solo actualización del compositor lejos?