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.
Comencemos observando algunos de los conceptos y clases clave que forman PhpSpec.
$ 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 elPhpSpec \ 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 laTema
. Empaca elTema
con el objeto real estamos especificando. Ya queTema
implementa elEnvoltura de interfaz
debe tener ungetWrappedObject ()
Método que nos da acceso al objeto. Esta es la instancia de objeto que buscábamos anteriormente conget_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 laTema
. También aprendimos queTema
es solo una envoltura y no el objeto real.Con este conocimiento, está claro que no podemos llamar
dumpThis ()
enTema
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 dePhpSpec \ 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. losLlamador
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 condumpThis ()
.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 unapartido()
método, que elcallExpectation ()
Llama para comprobar la expectativa. Hay cuatro tipos diferentes de expectativas:Positivo
,Negativo
,Positivo
yNegativo
. Cada una de estas expectativas contiene una instancia dePhpSpec \ Matcher \ MatcherInterface
que elpartido()
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…
ono 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 laMatcherInterface
. 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 elMatcherInterface
. 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 ()
, oshouldNotReturn ()
,no debería ser()
,shouldNotEqual ()
oshouldNotBeEqualTo ()
.Desde el
BasicMatcher
, Se heredan dos métodos:positivoMatch ()
yemparejamiento 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 elpartidos()
método (método abstracto que los emparejadores deben implementar) devuelvefalso
. losemparejamiento negativo ()
El método funciona de manera opuesta. lospartidos()
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 quepartidos()
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 deClase std
? Vamos a consultarget_class ()
una vez más:$ vendor / bin / phpspec ejecutar cadena (28) "PhpSpec \ Wrapper \ Collaborator"No En lugar de
Clase std
tenemos una instancia dePhpSpec \ Wrapper \ Collaborator
. De qué se trata esto?Me gusta
Tema
,Colaborador
es una envoltura e implementa elEnvoltura 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 unClase 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 contracierto
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..