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..
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..
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..
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 elGenerador
¿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);
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 uncarga automática
objeto, donde puede especificar qué directorios o clases autocargar. No mas desordenadoexigir
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,
Aunque las pruebas pasan, están tocando incorrectamente el sistema de archivos..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á.¿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 elExpediente
¿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 volverJohn 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 deExpediente
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 deldemoler
¿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 ladebería recibir (método)
ycon (ARG)
metodos.En este caso, cuando llamamos.
$ generar-> fuego ()
, Estamos afirmando que debería llamar alponer
método en elExpediente
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 tocarExpediente
. ¡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 WayValores 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 simuladoExpediente
clase, y debe, a los efectos de la ruta de esta prueba, devolvercierto
, señal de que el archivo ya existe y no debe sobrescribirse. A continuación, nos aseguramos de que, en situaciones como esta, laponer
método en elExpediente
La clase nunca se activa. Con Mockery, esto es fácil, gracias a laNunca()
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 probarExpediente
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 eslog.txt
ocache.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
comometro
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 $ timeoutSi 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 regresamos10000
.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, excluyendogetOption
, 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 convalor 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
Alternativamente, para que coincida con cualquier argumento, Burla :: cualquiera ()
podría convertirse cualquier cosa()
.
$ file-> shouldReceive ('put') -> with ('foo.txt', cualquier cosa ()) -> once ();
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?