Entendiendo phpSpec

Si compara PhpSpec con otros marcos de prueba, encontrará que es una herramienta muy sofisticada y de opinión. Una de las razones para esto, es que PhpSpec no es un marco de prueba como los que ya conoces. 

En cambio, es una herramienta de diseño que ayuda a describir el comportamiento del software. Un efecto secundario de describir el comportamiento del software con PhpSpec es que terminará con especificaciones que también servirán como pruebas después.

En este artículo, echaremos un vistazo bajo el capó de PhpSpec e intentaremos obtener una comprensión más profunda de cómo funciona y cómo usarlo..

Si desea mejorar phpspec, eche un vistazo a mi tutorial de introducción.

En este articulo…

  • Un recorrido rápido de PhpSpec Internals
  • La diferencia entre TDD y BDD
  • ¿En qué se diferencia PhpSpec (de PHPUnit)?
  • PhpSpec: una herramienta de diseño

Un recorrido rápido de PhpSpec Internals

Comencemos observando algunos de los conceptos y clases clave que forman PhpSpec.

Comprensión $ esto

Entendiendo que $ esto se refiere a que es clave para entender cómo PhpSpec se diferencia de otras herramientas. Básicamente, $ esto referirse a una instancia de la clase real bajo prueba. Intentemos investigar un poco más para comprender mejor lo que queremos decir..

En primer lugar, necesitamos una especificación y una clase para jugar. Como ustedes saben, los generadores de PhpSpec hacen que esto sea muy fácil para nosotros:

$ phpspec desc "Suhm \ HelloWorld" $ phpspec run ¿Quieres que cree 'Suhm \ HelloWorld' para ti? y 

A continuación, abra el archivo de especificaciones generado e intentemos obtener un poco más de información sobre $ esto:

shouldHaveType ('Suhm \ HelloWorld'); var_dump (get_class ($ this));  

get_class () devuelve el nombre de clase de un objeto dado. En este caso, acabamos de lanzar. $ esto allí para ver lo que devuelve:

$ string (24) "spec \ Suhm \ HelloWorldSpec"

Está bien, así que no es demasiado sorprendente., get_class () nos dice que $ esto es una instancia de spec \ Suhm \ HelloWorldSpec. Esto tiene sentido ya que, después de todo, esto es sólo código PHP viejo y sencillo. Si por el contrario usamos get_parent_class (), obtendríamosPhpSpec \ ObjectBehavior, ya que nuestra especificación extiende esta clase.

Recuerda, te acabo de decir eso. $ esto En realidad se refiere a la clase en prueba, que seríaSuhm \ HelloWorld ¿en nuestro caso? Como puede ver, el valor de retorno de get_class ($ esto) está en contradicción con $ this-> shouldHaveType ('Suhm \ HelloWorld');.

Probemos algo más:

shouldHaveType ('Suhm \ HelloWorld'); var_dump (get_class ($ this)); $ this-> dumpThis () -> shouldReturn ('spec \ Suhm \ HelloWorldSpec');  

Con el código anterior, intentamos llamar a un método llamado dumpThis () sobre el Hola Mundo ejemplo. Encadenamos una expectativa a la llamada al método, esperando que el valor de retorno de la función sea una cadena que contiene"spec \ Suhm \ HelloWorldSpec". Este es el valor de retorno de get_class () en la línea de arriba.

Nuevamente, los generadores de PhpSpec pueden ayudarnos con algunos andamios:

$ phpspec run ¿Quieres que cree 'Suhm \ HelloWorld :: dumpThis ()' para ti? y 

Vamos a tratar de llamar get_class () desde dentro dumpThis () también:

De nuevo, como es lógico, obtenemos:

 10 ✘ es inicializable esperado "spec \ Suhm \ HelloWorldSpec", pero tiene "Suhm \ HelloWorld". 

Parece que nos estamos perdiendo algo aquí. Comencé diciéndote que $ esto no se refiere a lo que crees que hace, pero hasta ahora nuestros experimentos no han mostrado nada inesperado. Excepto una cosa: ¿Cómo podríamos llamar $ this-> dumpThis () antes de que existiera sin que PHP nos chirriara?

Para entender esto, necesitamos sumergirnos en el código fuente de PhpSpec. Si quiere echar un vistazo usted mismo, puede leer el código en GitHub.

Echa un vistazo al siguiente código de src / PhpSpec / ObjectBehavior.php (La clase que nuestra especificación extiende):

/ ** * Proxies todas las llamadas al asunto PhpSpec * * @param string $ method * @param array $ argumentos * * @return mixed * / public function __call ($ method, array $ argumentos = array ()) return call_user_func_array ( array ($ this-> object, $ method), $ argumentos);  

Los comentarios regalan la mayor parte: "Proxies todos llaman al tema de PhpSpec". El PHP __llamada método es un método mágico llamado automáticamente cuando un método no es accesible (o no existe). 

Esto significa que cuando intentamos llamar $ this-> dumpThis (), La llamada aparentemente fue dirigida al tema PhpSpec. Si observa el código, puede ver que la llamada al método está dirigida a $ este-> objeto. (Lo mismo ocurre con las propiedades en nuestra instancia. Todos ellos están orientados al tema también, utilizando otros métodos mágicos. Echa un vistazo a la fuente para verlo por ti mismo).

Vamos a consultar get_class () Una vez más y ver qué tiene que decir al respecto. $ este-> objeto:

shouldHaveType ('Suhm \ HelloWorld'); var_dump (get_class ($ this-> object));  

Y mira lo que obtenemos:

cadena (23) "PhpSpec \ Wrapper \ Subject"

Más en Tema

Tema es una envoltura e implementa el PhpSpec \ Wrapper \ WrapperInterface. Es una parte fundamental de PhpSpec y permite toda la magia [aparentemente] que puede hacer el marco. Envuelve una instancia de la clase que estamos probando, para que podamos hacer todo tipo de cosas, como métodos de llamada y propiedades que no existen y establecer expectativas. 

Como se mencionó anteriormente, PhpSpec tiene muchas opiniones sobre cómo escribir y especificar su código. Una especificación se asigna a una clase. Solo tienes uno tema por especificación, que PhpSpec envolverá cuidadosamente para usted. Lo importante a tener en cuenta al respecto es que esto le permite utilizar $ esto como si fuera la instancia real y lo convierte en especificaciones realmente legibles y significativas.

PhpSpec contiene una Envoltura que se encarga de instanciar la Tema. Empaca el Tema con el objeto real estamos especificando. Ya que Tema implementa el Envoltura de interfaz debe tener un getWrappedObject ()Método que nos da acceso al objeto. Esta es la instancia de objeto que buscábamos anteriormente con get_class ()

Vamos a intentarlo de nuevo:

shouldHaveType ('Suhm \ HelloWorld'); var_dump (get_class ($ this-> object-> getWrappedObject ())); // Y solo para estar completamente seguro: var_dump ($ this-> object-> getWrappedObject () -> dumpThis ());  

Y ahí tienes:

$ vendor / bin / phpspec ejecutar cadena (15) "Suhm \ HelloWorld" cadena (15) "Suhm \ HelloWorld" 

A pesar de que muchas cosas están sucediendo detrás de la escena, al final seguimos trabajando con la instancia de objeto real de Suhm \ HelloWorld. Todo está bien.

Antes, cuando llamamos $ this-> dumpThis (), Aprendimos cómo la llamada fue en realidad proxy a la Tema. También aprendimos que Tema es solo una envoltura y no el objeto real. 

Con este conocimiento, está claro que no podemos llamar dumpThis () en Tema sin otro método mágico. Tema tiene un __llamada() método también:

/ ** * @param string $ method * @param array $ argumentos * * @return mixed | Subject * / public function __call ($ method, array $ argumentos = array ()) if (0 === strpos ($ method , 'should')) return $ this-> callExpectation ($ method, $ argumentos);  devolver $ this-> caller-> call ($ method, $ argumentos);  

Este método hace una de dos cosas. Primero, verifica si el nombre del método comienza con 'debería'. Si lo hace, es una expectativa, y la llamada se delega a un método llamado callExpectation (). Si no, la llamada se delega en una instancia de PhpSpec \ Wrapper \ Subject \ Caller

Vamos a ignorar el Llamador por ahora. También contiene el objeto envuelto y sabe cómo invocar métodos en él. los Llamador devuelve una instancia envuelta cuando llama a métodos sobre el tema, lo que nos permite encadenar las expectativas a los métodos, como hicimos con dumpThis ().

En su lugar, echemos un vistazo a la callExpectation () método:

/ ** * @param string $ method * @param array $ argumentos * * @return mixed * / private function callExpectation ($ method, array $ argumentos) $ subject = $ this-> makeSureWeHaveASubject (); $ expectation = $ this-> expectationFactory-> create ($ method, $ subject, $ argumentos); if (0 === strpos ($ method, 'shouldNot')) return $ expectation-> match (lcfirst (substr ($ method, 9)), $ this, $ argumentos, $ this-> wrappedObject);  return $ expectation-> match (lcfirst (substr ($ method, 6)), $ this, $ argumentos, $ this-> wrappedObject);  

Este método es responsable de construir una instancia de PhpSpec \ Wrapper \ Subject \ Expectation \ ExpectationInterface. Esta interfaz dicta una partido() método, que el callExpectation () Llama para comprobar la expectativa. Hay cuatro tipos diferentes de expectativas: PositivoNegativoPositivo y Negativo. Cada una de estas expectativas contiene una instancia de PhpSpec \ Matcher \ MatcherInterface que el partido() usos del método. Echemos un vistazo a los matchers siguiente.

Matchers

Los comparadores son lo que usamos para determinar el comportamiento de nuestros objetos. Cada vez que escribamos debería…  o no debería… , Estamos utilizando un emparejador. Puede encontrar una lista completa de los emparejadores de PhpSpec en mi blog personal.

Hay muchos emparejadores incluidos con PhpSpec, todos los cuales extienden la PhpSpec \ Matcher \ BasicMatcher clase, que implementa la MatcherInterface. La forma en que funcionan los emparejadores es bastante sencilla. Echémosle un vistazo juntos y te animo a que también eches un vistazo al código fuente.

Como ejemplo, veamos este código de la IdentityMatcher:

/ ** * @var array * / private static $ keywords = array ('return', 'be', 'equal', 'beEqualTo'); / ** * @param string $ nombre * @param mixed $ subject * @param array $ argumentos * * @return bool * / función pública admite ($ nombre, $ tema, matriz $ argumentos) return in_array ($ nombre, auto :: $ keywords) && 1 == count ($ argumentos);  

los apoya () El método es dictado por el MatcherInterface. En este caso, cuatro. alias Se definen para el matcher en el $ palabras clave formación. Esto permitirá que el emparejador admita: debería volver ()debiera ser()shouldEqual () oshouldBeEqualTo (), o shouldNotReturn ()no debería ser()shouldNotEqual () o shouldNotBeEqualTo ().

Desde el BasicMatcher, Se heredan dos métodos: positivoMatch () y emparejamiento negativo (). Se ven así:

/ ** * @param string $ name * @param mixed $ subject * @param array $ argumentos * * @return mixed * * @throws FailureException * / final public function positiveMatch ($ name, $ subject, array $ argumentos) si (false === $ this-> match ($ subject, $ argumentos)) throw $ this-> getFailureException ($ nombre, $ tema, $ argumentos);  return $ asunto;  

los positivoMatch () método lanza una excepción si el partidos() método (método abstracto que los emparejadores deben implementar) devuelve falso. los emparejamiento negativo () El método funciona de manera opuesta. los partidos() método para elIdentityMatcher usa el === operador para comparar el $ sujeto con el argumento suministrado al método de comparación:

/ ** * @param mixed $ subject * @param array $ argumentos * * @return bool * / coincidencias de funciones protegidas ($ subject, array $ argumentos) return $ subject === $ argumentos [0];  

Podríamos usar el matcher así:

$ this-> getUser () -> shouldNotBeEqualTo ($ anotherUser); 

Lo que eventualmente llamaría emparejamiento negativo () y asegúrate de que partidos() devuelve falso.

Eche un vistazo a algunos de los otros competidores y vea lo que hacen.!

Promesas de más magia

Antes de terminar este breve recorrido por las partes internas de PhpSpec, echemos un vistazo a una pieza más de magia:

shouldHaveType ('Suhm \ HelloWorld'); var_dump (get_class ($ object));  

Añadiendo el tipo insinuado. $ objeto parámetro a nuestro ejemplo, PhpSpec usará automáticamente la reflexión para inyectar una instancia de la clase para que la usemos. Pero con las cosas que ya vimos, ¿realmente confiamos en que realmente obtenemos una instancia de Clase std? Vamos a consultar get_class () una vez más:

$ vendor / bin / phpspec ejecutar cadena (28) "PhpSpec \ Wrapper \ Collaborator" 

No En lugar de Clase std tenemos una instancia de PhpSpec \ Wrapper \ Collaborator. De qué se trata esto?

Me gusta TemaColaborador es una envoltura e implementa el Envoltura de interfaz. Se envuelve una instancia de\ Prophecy \ Prophecy \ ObjectProphecy, que se deriva de la profecía, el marco de burla que se une con PhpSpec. En lugar de un Clase std Por ejemplo, PhpSpec nos da un simulacro. Esto hace que burlarse de risa sea fácil con PhpSpec y nos permite agregar promesas a nuestros objetos de esta manera:

$ user-> getAge () -> willReturn (10); $ this-> setUser ($ usuario); $ this-> getUserStatus () -> shouldReturn ('child'); 

Con este breve recorrido por partes de las partes internas de PhpSpec, espero que vea que es más que un simple marco de prueba.

La diferencia entre TDD y BDD

PhpSpec es una herramienta para realizar SpecBDD, por lo que para obtener una mejor comprensión, echemos un vistazo a las diferencias entre el desarrollo guiado por pruebas (TDD) y el desarrollo guiado por el comportamiento (BDD). Luego, veremos rápidamente cómo PhpSpec se diferencia de otras herramientas como PHPUnit..

TDD es el concepto de permitir que las pruebas automatizadas conduzcan el diseño y la implementación del código. Al escribir pruebas pequeñas para cada característica, antes de implementarlas realmente, cuando obtenemos una prueba de aprobación, sabemos que nuestro código satisface esa característica específica. Con una prueba que pasa, después de la refactorización, dejamos de codificar y escribimos la siguiente prueba en su lugar. El mantra es "rojo", "verde", "refactor"!

BDD tiene su origen desde - y es muy similar a - TDD. Honestamente, se trata principalmente de una redacción, lo que es realmente importante, ya que puede cambiar nuestra forma de pensar como desarrolladores. Donde TDD habla sobre pruebas, BDD habla sobre describir el comportamiento. 

Con TDD nos enfocamos en verificar que nuestro código funciona de la manera que esperamos que funcione, mientras que con BDD, nos enfocamos en verificar que nuestro código realmente se comporte de la manera que queremos. Una de las razones principales para la aparición de la BDD, como alternativa a la TDD, es evitar el uso de la palabra "prueba". Con BDD no estamos realmente interesados ​​en probar la implementación de nuestro código, estamos más interesados ​​en probar lo que hace (su comportamiento). Cuando hacemos BDD, en lugar de TDD, tenemos historias y especificaciones. Esto hace que las pruebas tradicionales sean redundantes..

Las historias y las especificaciones están estrechamente relacionadas con las expectativas de las partes interesadas del proyecto. Escribir historias (con una herramienta como Behat), preferiblemente se realizaría junto con las partes interesadas o expertos en el dominio. Las historias cubren el comportamiento externo. Usamos especificaciones para diseñar el comportamiento interno necesario para completar los pasos de las historias. Cada paso en una historia puede requerir múltiples iteraciones con especificaciones de escritura y código de implementación, antes de que se satisfaga. Nuestras historias, junto con nuestras especificaciones, nos ayudan a asegurarnos de que no solo estamos construyendo algo que funcione, sino que también es lo correcto. Como tal, BDD tiene mucho que ver con la comunicación..

¿En qué se diferencia PhpSpec de PHPUnit??

Hace unos meses, un miembro notable de la comunidad de PHP, Mathias Verraes, publicó "Un marco de prueba de unidad en un tweet" en Twitter. El punto era encajar el código fuente de un marco de prueba de unidad funcional en un solo tweet. Como puede ver en la esencia, el código es verdaderamente funcional y le permite escribir pruebas básicas de unidades. El concepto de prueba unitaria es en realidad bastante simple: verifique algún tipo de afirmación y notifique al usuario el resultado.

Por supuesto, la mayoría de los marcos de prueba, como PHPUnit, son de hecho mucho más avanzados y pueden hacer mucho más que el marco de Mathias, pero todavía muestra un punto importante: usted afirma algo y luego el marco ejecuta esa afirmación por usted..

Echemos un vistazo a una prueba PHPUnit muy básica:

función pública testTrue () $ this-> assertTrue (false);  

¿Sería capaz de escribir una implementación super simple de un marco de prueba que podría ejecutar esta prueba? Estoy bastante seguro de que la respuesta es "sí" que podría hacer eso. Después de todo, lo único que assertTrue () método que tiene que hacer es comparar un valor contra cierto y lanza una excepción si falla. En su esencia, lo que está pasando es bastante sencillo..

Entonces, ¿cómo es diferente PhpSpec? En primer lugar, PhpSpec no es una herramienta de prueba. Probar su código no es el objetivo principal de PhpSpec, pero se convierte en un efecto secundario si lo usa para diseñar su software agregando incrementalmente especificaciones para el comportamiento (BDD). 

En segundo lugar, creo que las secciones anteriores ya deberían haber dejado en claro cómo PhpSpec es diferente. Aún así, vamos a comparar algunos códigos:

// PhpSpec function it_is_initializable () $ this-> shouldHaveType ('Suhm \ HelloWorld');  // PHPUnit function testIsInitializable () $ object = new Suhm \ HelloWorld (); $ this-> assertInstanceOf ('Suhm \ HelloWorld', $ object);  

Debido a que PhpSpec tiene una gran opinión y hace algunas afirmaciones sobre cómo está diseñado nuestro código, nos proporciona una manera muy fácil de describir nuestro código. Por otro lado, PHPUnit no hace ninguna afirmación hacia nuestro código y nos permite hacer casi lo que queremos. Básicamente, todo lo que PHPUnit hace por nosotros en este ejemplo es ejecutar $ objeto en contra deen vez de operador. 

Aunque parezca más fácil comenzar con PHPUnit (no creo que lo sea), si no tiene cuidado, puede caer fácilmente en trampas de mal diseño y arquitectura porque le permite hacer casi cualquier cosa. Dicho esto, PHPUnit aún puede ser excelente para muchos casos de uso, pero no es una herramienta de diseño como PhpSpec. No hay orientación, tienes que saber lo que estás haciendo..

PhpSpec: una herramienta de diseño

Desde el sitio web de PhpSpec, podemos aprender que PhpSpec es:

Un conjunto de herramientas php para impulsar el diseño emergente por especificación.

Permítanme decirlo una vez más: PhpSpec no es un marco de prueba. Es una herramienta de desarrollo. Una herramienta de diseño de software. No es un marco de aserción simple que compara valores y lanza excepciones. Es una herramienta que nos ayuda a diseñar y construir códigos bien diseñados. Requiere que pensemos en la estructura de nuestro código y apliquemos ciertos patrones arquitectónicos, donde una clase se asigna a una especificación. Si rompe el principio de responsabilidad única y necesita burlarse parcialmente de algo, no se le permitirá hacerlo..

Feliz especificación!

Oh! Y, por último, desde que se especificó PhpSpec, sugiero que vaya a GitHub y explore la fuente para obtener más información..