En la segunda parte de esta serie llamada Laravel, BDD and You, comenzaremos a describir y construir nuestra primera característica utilizando Behat y PhpSpec. En el último artículo, configuramos todo y vimos con qué facilidad podemos interactuar con Laravel en nuestros escenarios de Behat..
Recientemente, el creador de Behat, Konstantin Kudryashov (a.k.a. everzet), escribió un artículo realmente genial llamado Introducción al modelado por ejemplo. El flujo de trabajo que vamos a utilizar, cuando construyamos nuestra función, está muy inspirado en el que presenta everzet.
En resumen, vamos a utilizar el mismo .característica
para diseñar tanto nuestro dominio central como nuestra interfaz de usuario. A menudo sentí que tenía muchas duplicaciones en mis funciones en mis suites de aceptación / funcional e integración. Cuando leí la sugerencia de everzet sobre el uso de la misma función para múltiples contextos, todo hizo clic para mí y creo que es el camino a seguir..
En nuestro caso, tendremos nuestro contexto funcional, que, por ahora, también servirá como nuestra capa de aceptación y nuestro contexto de integración, que cubrirá nuestro dominio. Comenzaremos por construir el dominio y luego agregaremos la IU y los elementos específicos del marco de trabajo posteriormente..
Para poder utilizar el enfoque de "características compartidas, contextos múltiples", tenemos que hacer algunas refactorizaciones de nuestra configuración existente.
Primero, vamos a eliminar la función de bienvenida que hicimos en la primera parte, ya que realmente no la necesitamos y realmente no sigue el estilo genérico que necesitamos para utilizar múltiples contextos..
$ git rm features / funcional / welcome.feature
En segundo lugar, vamos a tener nuestras características en la raíz de la caracteristicas
carpeta, por lo que podemos seguir adelante y eliminar el camino
atributo de nuestro behat.yml
expediente. También vamos a cambiar el nombre de LaravelFeatureContext
a FuncionalFeatureContext
(recuerda cambiar el nombre de la clase también):
predeterminado: suites: funcional: contextos: [FunctionalFeatureContext]
Finalmente, solo para limpiar un poco las cosas, creo que deberíamos mover todas las cosas relacionadas con Laravel a su propio rasgo:
# features / bootstrap / LaravelTrait.php aplicación) $ this-> refreshApplication (); / ** * Crea la aplicación. * * @return \ Symfony \ Component \ HttpKernel \ HttpKernelInterface * / public function createApplication () $ unitTesting = true; $ testEnvironment = 'testing'; el retorno requiere __DIR __. '/… /… /bootstrap/start.php';
En el FuncionalFeatureContext
luego podemos usar el rasgo y eliminar las cosas que acabamos de mover:
/ ** * Clase de contexto de Behat. * / class FunctionalFeatureContext implementa SnippetAcceptingContext use LaravelTrait; / ** * Inicializa el contexto. * * Cada escenario tiene su propio objeto de contexto. * También puede pasar argumentos arbitrarios al constructor de contexto a través de behat.yml. * / public function __construct ()
Los rasgos son una gran manera de limpiar sus contextos.
Como se presentó en la primera parte, vamos a construir una pequeña aplicación para el seguimiento del tiempo. La primera característica será el tiempo de seguimiento y la generación de una hoja de tiempo de las entradas rastreadas. Aquí está la característica:
Característica: tiempo de seguimiento Para hacer un seguimiento del tiempo dedicado a las tareas Como empleado, necesito administrar una hoja de tiempo con entradas de tiempo. Escenario: Generar hoja de tiempo. Dado que tengo las siguientes entradas de tiempo | tarea | duración | | codificación | 90 | | codificación | 30 | | documentando | 150 | Cuando genero la hoja de tiempo, mi tiempo total dedicado a la codificación debería ser de 120 minutos Y el total de mi tiempo dedicado a la documentación debería ser de 150 minutos Y el total de mi tiempo dedicado a las reuniones debería ser de 0 minutos Y el total del tiempo empleado debería ser de 270 minutos
Recuerda que esto es solo un ejemplo. Me resulta más fácil definir las características en la vida real, ya que tiene un problema real que necesita resolver y, a menudo, tiene la oportunidad de discutir la característica con colegas, clientes u otras partes interesadas..
Bien, dejemos que Behat genere los pasos del escenario para nosotros:
$ vendor / bin / behat --dry-run --append-snippets
Necesitamos ajustar los pasos generados un poquito. Solo necesitamos cuatro pasos para cubrir el escenario. El resultado final debe verse algo como esto:
/ ** * @Given Tengo las siguientes entradas de tiempo * / public function iHaveTheFollowingTimeEntries (TableNode $ table) throw new PendingException (); / ** * @Cuando genero la hoja de tiempo * / public function iGenerateTheTimeSheet () lanzo una nueva PendingException (); / ** * @Entonces mi tiempo total dedicado a: tarea debería ser: minutos de duración esperados * / función pública myTotalTimeSpentOnTaskShouldBeMinutes ($ task, $ expectedDuration) arrojar una nueva PendingException (); / ** * @Entonces mi tiempo total empleado debería ser: esperadoDurantes minutos * / función pública myTotalTimeSpentShouldBeMinutes ($ expectedDuration) lanzar una nueva PendingException ();
Nuestro contexto funcional está listo para funcionar ahora, pero también necesitamos un contexto para nuestra suite de integración. Primero, añadiremos la suite a la behat.yml
expediente:
predeterminado: suites: funcional: contextos: [FunctionalFeatureContext] integración: contextos: [IntegrationFeatureContext]
A continuación, solo podemos copiar el predeterminado FeatureContext
:
$ cp features / bootstrap / FeatureContext.php features / bootstrap / IntegrationFeatureContext.php
Recuerde cambiar el nombre de la clase a IntegraciónFeaturaContexto
y también para copiar la declaración de uso para el PendingException
.
Finalmente, ya que estamos compartiendo la función, solo podemos copiar las definiciones de los cuatro pasos del contexto funcional. Si ejecuta Behat, verá que la función se ejecuta dos veces: una vez para cada contexto.
En este punto, estamos listos para comenzar a completar los pasos pendientes en nuestro contexto de integración para diseñar el dominio central de nuestra aplicación. El primer paso es Dado que tengo las siguientes entradas de tiempo
, seguido de una tabla con registros de entrada de tiempo. Manteniéndolo simple, simplemente recorramos las filas de la tabla, intentemos crear una instancia de tiempo para cada una de ellas y agregarlas a una matriz de entradas en el contexto:
use TimeTracker \ TimeEntry;… / ** * @Given Tengo las siguientes entradas de tiempo * / public function iHaveTheFollowingTimeEntries (TableNode $ table) $ this-> entries = []; $ rows = $ table-> getHash (); foreach ($ rows as $ row) $ entry = new TimeEntry; $ entry-> task = $ row ['task']; $ entry-> duration = $ row ['duration']; $ this-> entries [] = $ entry;
La ejecución de Behat causará un error fatal, ya que TimeTracker \ TimeEntry
La clase aún no existe. Aquí es donde PhpSpec entra en escena. En el final, TimeEntry
va a ser una clase elocuente, aunque todavía no nos preocupemos por eso. PhpSpec y ORM como Eloquent no juegan juntos tan bien, pero aún podemos usar PhpSpec para generar la clase e incluso especificar algunos comportamientos básicos. Usemos los generadores de PhpSpec para generar el TimeEntry
clase:
$ vendor / bin / phpspec desc "TimeTracker \ TimeEntry" $ vendor / bin / phpspec run ¿Quieres que cree 'TimeTracker \ TimeEntry' para ti? y
Una vez generada la clase, necesitamos actualizar la sección de carga automática de nuestra compositor.json
expediente:
"autoload": "classmap": ["aplicación / comandos", "aplicación / controladores", "aplicación / modelos", "aplicación / base de datos / migraciones", "aplicación / base de datos / semillas"], "psr-4" : "TimeTracker \\": "src / TimeTracker",
Y por supuesto correr compositor dump-autoload
.
Ejecutando PhpSpec nos da verde. Correr Behat también nos da verde. Que gran comienzo!
Dejando que Behat guíe nuestro camino, ¿qué tal si simplemente nos movemos hacia el siguiente paso?, Cuando genero la hoja de tiempo
, inmediatamente?
La palabra clave aquí es "generar", que parece un término de nuestro dominio. En el mundo de un programador, traducir "generar la hoja de tiempo" al código podría significar simplemente crear una instancia de un TimeSheet
clase con un montón de entradas de tiempo. Es importante tratar de mantener el idioma del dominio cuando diseñamos nuestro código. De esa manera, nuestro código ayudará a describir el comportamiento previsto de nuestra aplicación..
Yo identifico el termino generar
tan importante para el dominio, por lo que creo que deberíamos tener un método de generación estática en un TimeSheet
Clase que sirve de alias para el constructor. Este método debe tomar una colección de entradas de tiempo y almacenarlas en la hoja de tiempo.
En lugar de solo usar una matriz, creo que tendrá sentido usar el Illuminate \ Support \ Collection
Clase que viene con Laravel. Ya que TimeEntry
será un modelo Eloquent, cuando consultemos la base de datos para las entradas de tiempo, obtendremos una de estas colecciones de Laravel de todos modos. Qué tal algo como esto:
utilizar Illuminate \ Support \ Collection; use TimeTracker \ TimeSheet; use TimeTracker \ TimeEntry;… / ** * @ Cuando genero la hoja de tiempo * / public function iGenerateTheTimeSheet () $ this-> sheet = TimeSheet :: generate (Collection :: make ($ this-> entries));
Por cierto, TimeSheet no va a ser una clase de Eloquent. Al menos por ahora, solo tenemos que hacer que las entradas de tiempo persistan, y luego las hojas de tiempo solo serán generado de las entradas.
La ejecución de Behat, una vez más, causará un error fatal, porque TimeSheet
no existe. PhpSpec puede ayudarnos a resolver eso:
$ vendor / bin / phpspec desc "TimeTracker \ TimeSheet" $ vendedor / bin / phpspec run ¿Desea que cree 'TimeTracker \ TimeSheet' para usted? y $ vendor / bin / phpspec ejecuta $ vendor / bin / behat PHP Error grave: llamar al método no definido TimeTracker \ TimeSheet :: generate ()
Seguimos recibiendo un error fatal después de crear la clase, porque la estática generar()
El método todavía no existe. Dado que este es un método estático realmente simple, no creo que haya necesidad de una especificación. No es nada más que una envoltura para el constructor:
entradas = $ entradas; generar la función estática pública (entradas de Collection $) devolver nueva estática (entradas de $);
Esto hará que Behat vuelva a ser verde, pero PhpSpec ahora nos está hablando con fuerza, diciendo: El argumento 1 pasado a TimeTracker \ TimeSheet :: __ construct () debe ser una instancia de Illuminate \ Support \ Collection, ninguna dada
. Podemos resolver esto escribiendo un simple dejar()
Función que se llamará ante cada especificación:
put (new TimeEntry); $ this-> beConstructedWith ($ entries); función it_is_initializable () $ this-> shouldHaveType ('TimeTracker \ TimeSheet');
Esto nos hará volver a verde en toda la línea. La función se asegura de que la hoja de tiempo siempre se construya con un simulacro de la clase Colección..
Ahora podemos pasar con seguridad a la Entonces mi tiempo total dedicado a ...
paso. Necesitamos un método que tome un nombre de tarea y devuelva la duración acumulada de todas las entradas con este nombre de tarea. Traducido directamente del pepinillo al código, esto podría ser algo así como totalTimeSpentOn ($ tarea)
:
/ ** * @Entonces mi tiempo total dedicado a: tarea debería ser: minutos de duración esperados * / función pública myTotalTimeSpentOnTaskShouldBeMinutes ($ task, $ expectedDuration) $ actualDuration = $ this-> sheet-> totalTimeSpentOn ($ task); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration);
El método no existe, por lo que ejecutar Behat nos dará Llamada al método no definido TimeTracker \ TimeSheet :: totalTimeSpentOn ()
.
Para especificar el método, escribiremos una especificación que se parece de alguna manera a la que ya tenemos en nuestro escenario:
function it_should_calculate_total_time_spent_on_task () $ entry1 = new TimeEntry; $ entry1-> task = 'sleeping'; $ entry1-> duración = 120; $ entry2 = new TimeEntry; $ entry2-> task = 'eating'; $ entry2-> duración = 60; $ entry3 = new TimeEntry; $ entry3-> task = 'sleeping'; $ entry3-> duración = 120; $ collection = Collection :: make ([$ entry1, $ entry2, $ entry3]); $ this-> beConstructedWith ($ collection); $ this-> totalTimeSpentOn ('sleeping') -> shouldBe (240); $ this-> totalTimeSpentOn ('comiendo') -> shouldBe (60);
Tenga en cuenta que no utilizamos simulacros para el TimeEntry
y Colección
instancias. Esta es nuestra suite de integración y no creo que sea necesario burlarse de esto. Los objetos son bastante simples y queremos asegurarnos de que los objetos en nuestro dominio interactúen como esperamos. Probablemente hay muchas opiniones sobre esto, pero esto tiene sentido para mí..
Moviéndose a lo largo:
$ vendor / bin / phpspec run ¿Desea que cree 'TimeTracker \ TimeSheet :: totalTimeSpentOn ()' para usted? y $ vendor / bin / phpspec run 25 ✘ debería calcular el tiempo total empleado en la tarea esperada [entero: 240], pero se anuló.
Para filtrar las entradas, podemos utilizar el filtrar()
método en el Colección
clase. Una solución simple que nos pone verde:
public function totalTimeSpentOn ($ task) $ entries = $ this-> entries-> filter (function ($ entry) use ($ task) return $ entry-> task === $ task;); $ duration = 0; foreach ($ entries como $ entry) $ duration + = $ entry-> duration; return $ duration;
Nuestra especificación es verde, pero creo que podríamos beneficiarnos de una refactorización aquí. El método parece hacer dos cosas diferentes: filtrar entradas y acumular la duración. Extraigamos este último a su propio método:
public function totalTimeSpentOn ($ task) $ entries = $ this-> entries-> filter (function ($ entry) use ($ task) return $ entry-> task === $ task;); devuelve $ this-> sumDuration ($ entries); función protegida sumDuration ($ entries) $ duration = 0; foreach ($ entradas como $ entrada) $ duration + = $ entry-> duration; return $ duration;
PhpSpec sigue siendo verde y ahora tenemos tres pasos verdes en Behat. El último paso debe ser fácil de implementar, ya que es algo similar al que acabamos de hacer..
/ ** * @Entonces mi tiempo total dedicado debería ser: esperadoDurantes minutos * / función pública myTotalTimeSpentShouldBeMinutes ($ expectedDuration) $ actualDuration = $ this-> sheet-> totalTimeSpent (); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration);
Correr Behat nos dará Llamada al método no definido TimeTracker \ TimeSheet :: totalTimeSpent ()
. En lugar de hacer un ejemplo separado en nuestra especificación para este método, ¿qué tal si simplemente lo agregamos al que ya tenemos? Puede que no esté completamente en línea con lo que es "correcto" hacer, pero seamos un poco pragmáticos:
… $ This-> beConstructedWith ($ collection); $ this-> totalTimeSpentOn ('sleeping') -> shouldBe (240); $ this-> totalTimeSpentOn ('comiendo') -> shouldBe (60); $ this-> totalTimeSpent () -> shouldBe (300);
Deje que PhpSpec genere el método:
$ vendor / bin / phpspec run ¿Desea que cree 'TimeTracker \ TimeSheet :: totalTimeSpent ()' para usted? y $ vendor / bin / phpspec run 25 ✘ debería calcular el tiempo total empleado en la tarea esperada [entero: 300], pero se anuló.
Llegar a verde es fácil ahora que tenemos la sumDuration ()
método:
función pública totalTimeSpent () return $ this-> sumDuration ($ this-> entries);
Y ahora tenemos una característica verde. Nuestro dominio está evolucionando lentamente.!
Ahora, nos estamos moviendo a nuestra suite funcional. Vamos a diseñar la interfaz de usuario y tratar con todas las cosas específicas de Laravel que no son la preocupación de nuestro dominio..
Mientras trabajamos en la suite funcional, podemos agregar el -s
para indicar a Behat que solo ejecute nuestras funciones a través de FuncionalFeatureContext
:
$ vendor / bin / behat -s funcional
El primer paso será similar al primero del contexto de integración. En lugar de simplemente hacer que las entradas permanezcan en el contexto en una matriz, necesitamos hacerlas persistir en una base de datos para que puedan recuperarse más adelante:
use TimeTracker \ TimeEntry;… / ** * @Given Tengo las siguientes entradas de tiempo * / public function iHaveTheFollowingTimeEntries (TableNode $ table) $ rows = $ table-> getHash (); foreach ($ rows as $ row) $ entry = new TimeEntry; $ entry-> task = $ row ['task']; $ entry-> duration = $ row ['duration']; $ entrada-> guardar ();
Correr Behat nos dará error fatal Llamar al método indefinido TimeTracker \ TimeEntry :: save ()
, ya que TimeEntry
Todavía no es un modelo elocuente. Eso es fácil de arreglar:
espacio de nombres TimeTracker; la clase TimeEntry extiende \ Eloquent
Si volvemos a ejecutar Behat, Laravel se quejará de que no puede conectarse a la base de datos. Podemos arreglar esto agregando un database.php
archivo a la app / config / testing
Directorio, con el fin de agregar los detalles de conexión para nuestra base de datos. Para proyectos más grandes, probablemente desee utilizar el mismo servidor de base de datos para sus pruebas y su base de código de producción, pero en nuestro caso, solo utilizaremos una base de datos SQLite en memoria. Esto es super simple de configurar con Laravel:
'sqlite', 'connections' => array ('sqlite' => array ('driver' => 'sqlite', 'database' => ': memory:', 'prefix' => ",),),) ;
Ahora si corremos Behat, nos dirá que no hay time_entries
mesa. Para arreglar esto, necesitamos hacer una migración:
$ php artisan migrate: make createTimeEntriesTable --create = "time_entries"
Schema :: create ('time_entries', function (Blueprint $ table) $ table-> incrementos ('id'); $ table-> string ('task'); $ table-> integer ('duration'); $ table-> timestamps (););
Todavía no estamos en verde, ya que necesitamos una manera de instruir a Behat para que ejecute nuestras migraciones antes de cada escenario, por lo que siempre tenemos una pizarra limpia. Al usar las anotaciones de Behat, podemos agregar estos dos métodos a la LaravelTrait
rasgo:
/ ** * @BeforeScenario * / public function setupDatabase () $ this-> app ['artisan'] -> call ('migrate'); / ** * @AfterScenario * / public function cleanDatabase () $ this-> app ['artisan'] -> call ('migrate: reset');
Esto es bastante bueno y da nuestro primer paso a verde.
El siguiente es el Cuando genero la hoja de tiempo
paso. La forma en que lo veo, generar la hoja de tiempo es el equivalente a visitar el índice
Acción del recurso de entrada de tiempo, ya que la hoja de tiempo es la colección de todas las entradas de tiempo. Por lo tanto, el objeto de la hoja de tiempo es como un contenedor para todas las entradas de tiempo y nos brinda una buena manera de manejar las entradas. En lugar de ir a / tiempo-entradas
, Para ver la hoja de tiempo, creo que el empleado debe ir a / hoja de tiempo
. Debemos poner eso en nuestra definición de paso:
/ ** * @Cuando genero la hoja de tiempo * / public function iGenerateTheTimeSheet () $ this-> call ('GET', '/ time-sheet'); $ this-> crawler = new Crawler ($ this-> client-> getResponse () -> getContent (), url ('/'));
Esto causará un NotFoundHttpException
, ya que la ruta aún no está definida. Como acabo de explicar, creo que esta URL debería corresponder a la índice
Acción sobre el recurso de entrada de tiempo:
Route :: get ('time-sheet', ['as' => 'time_sheet', 'uses' => 'TimeEntriesController @ index']);
Para llegar a verde, necesitamos generar el controlador:
$ php controlador artesanal: haga que TimeEntriesController $ composer dump-autoload
Y ahí vamos.
Finalmente, necesitamos rastrear la página para encontrar la duración total de las entradas de tiempo. Calculo que tendremos algún tipo de tabla que resuma las duraciones. Los dos últimos pasos son tan similares que solo los implementaremos al mismo tiempo:
/ ** * @Entonces mi tiempo total dedicado a: tarea debería ser: minutos de duración esperados * / función pública myTotalTimeSpentOnTaskShouldBeMinutes ($ task, $ expectedDuration) $ actualDuration = $ this-> crawler-> filter ('td #'. $ Task . 'TotalDuration') -> text (); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration); / ** * @Entonces mi tiempo total empleado debería ser: expectedDuration minutes * / public function myTotalTimeSpentShouldBeMinutes ($ expectedDuration) $ actualDuration = $ this-> crawler-> filter ('td # totalDuration') -> text (); PHPUnit :: assertEquals ($ expectedDuration, $ actualDuration);
El rastreador está buscando un Como todavía no tenemos una vista, el rastreador nos dirá que Para arreglar esto, construyamos el La vista, por ahora, solo consistirá en una tabla simple con los valores de duración resumidos: Si ejecuta Behat nuevamente, verá que implementamos exitosamente la función. ¡Tal vez deberíamos tomarnos un momento para darnos cuenta de que ni siquiera una vez abrimos un navegador! Esta es una mejora masiva de nuestro flujo de trabajo, y como una ventaja adicional, ahora tenemos pruebas automatizadas para nuestra aplicación. Hurra! Si tu corres Si ejecuta estos dos comandos: Verá que hemos vuelto a verde, tanto con Behat como con PhpSpec. Hurra! Ahora hemos descrito y diseñado nuestra primera característica, utilizando completamente un enfoque BDD. Hemos visto cómo podemos beneficiarnos del diseño del dominio central de nuestra aplicación, antes de preocuparnos por la interfaz de usuario y las cosas específicas del marco. También hemos visto lo fácil que es interactuar con Laravel, y especialmente con la base de datos, en nuestros contextos de Behat.. En el próximo artículo, vamos a hacer una gran cantidad de refactorización para evitar demasiada lógica en nuestros modelos Eloquent, ya que son más difíciles de probar de forma aislada y están estrechamente relacionados con Laravel. Manténganse al tanto! nodo con un id de [task_name] TotalDuration
o duración total
en el ultimo ejemplo.La lista de nodos actual está vacía.
índice
acción. Primero, buscamos la colección de entradas de tiempo. Segundo, generamos una hoja de tiempo de las entradas y la enviamos a la vista (aún no existente).use TimeTracker \ TimeSheet; use TimeTracker \ TimeEntry; la clase TimeEntriesController extiende \ BaseController / ** * Muestra una lista del recurso. * * @return Response * / índice de función pública () $ entries = TimeEntry :: all (); $ sheet = TimeSheet :: genera ($ entradas); return View :: make ('time_entries.index', compact ('sheet')); …
Hoja de tiempo
Tarea Duración total codificación $ sheet-> totalTimeSpentOn ('codificación') documentando $ sheet-> totalTimeSpentOn ('documenting') reuniones $ hoja-> totalTimeSpentOn ('reuniones') Total $ sheet-> totalTimeSpent () Conclusión
vendedor / bin / behat
para ejecutar las dos suites Behat, verás que ambas están verdes ahora. Si ejecuta PhpSpec, desafortunadamente, verá que nuestras especificaciones están dañadas. Recibimos un error fatal La clase 'Elocuente' no se encuentra en ...
. Esto es porque Eloquent es un alias. Si echas un vistazo app / config / app.php
bajo alias, veras que Elocuente
es en realidad un alias para Illuminate \ Database \ Eloquent \ Model
. Para que PhpSpec vuelva a verde, necesitamos importar esta clase:espacio de nombres TimeTracker; use Illuminate \ Database \ Eloquent \ Model como Eloquent; la clase TimeEntry extiende Eloquent
$ vendor / bin / phpspec run; vendedor / bin / behat