Dans la deuxième partie de cette série intitulée Laravel, BDD and You, nous commencerons à décrire et à construire notre premier long métrage utilisant Behat et PhpSpec. Dans le dernier article, nous avons tout préparé et avons vu à quel point nous pouvons facilement interagir avec Laravel dans nos scénarios Behat..
Récemment, le créateur de Behat, Konstantin Kudryashov (ak.a. everzet), a écrit un très bon article intitulé Introducing Modeling by Example. Le flux de travail que nous allons utiliser lors de la création de notre fonctionnalité est fortement inspiré de celui présenté par everzet..
En bref, nous allons utiliser le même .fonctionnalité
pour concevoir à la fois notre domaine de base et notre interface utilisateur. J'ai souvent eu le sentiment que mes fonctionnalités étaient dupliquées dans mes suites d'acceptation / fonctionnelles et d'intégration. Lorsque j'ai lu la suggestion de everzet concernant l'utilisation de la même fonctionnalité dans plusieurs contextes, tout a cliqué pour moi et je crois que c'est la voie à suivre..
Dans notre cas, nous aurons notre contexte fonctionnel, qui servira également pour l'instant de couche d'acceptation, et notre contexte d'intégration, qui couvrira notre domaine. Nous allons commencer par construire le domaine, puis ajouter ensuite l'interface utilisateur et les éléments spécifiques à la structure..
Pour utiliser l'approche "fonctionnalité partagée, contextes multiples", nous devons effectuer quelques modifications de notre configuration existante..
Tout d'abord, nous allons supprimer la fonctionnalité de bienvenue que nous avons créée dans la première partie, car nous n'en avons pas vraiment besoin et elle ne suit pas vraiment le style générique dont nous avons besoin pour utiliser plusieurs contextes..
$ git rm features / functional / welcome.feature
Deuxièmement, nous allons avoir nos fonctionnalités à la racine du fonctionnalités
dossier, afin que nous puissions aller de l'avant et supprimer le chemin
attribut de notre behat.yml
fichier. Nous allons également renommer le LaravelFeatureContext
à FunctionalFeatureContext
(n'oubliez pas de changer également le nom de la classe):
défaut: suites: fonctionnel: contextes: [FunctionalFeatureContext]
Enfin, juste pour nettoyer un peu les choses, je pense que nous devrions déplacer tous les éléments liés à Laravel dans leur propre trait:
# features / bootstrap / LaravelTrait.php app) $ this-> refreshApplication (); / ** * Crée l'application. * * @return \ Symfony \ Component \ HttpKernel \ HttpKernelInterface * / fonction publique createApplication () $ unitTesting = true; $ testEnvironment = 'testing'; return require __DIR __. '/… /… /bootstrap/start.php';
dans le FunctionalFeatureContext
nous pouvons ensuite utiliser le trait et supprimer les éléments que nous venons de déplacer:
/ ** * Classe de contexte Behat. * / class FunctionalFeatureContext implémente SnippetAcceptingContext use LaravelTrait; / ** * Initialise le contexte. * * Chaque scénario obtient son propre objet de contexte. * Vous pouvez également transmettre des arguments arbitraires au constructeur de contexte via behat.yml. * / fonction publique __construct ()
Les traits sont un excellent moyen de nettoyer vos contextes.
Comme présenté dans la première partie, nous allons créer une petite application de suivi du temps. La première fonction concerne le suivi du temps et la génération d’une feuille de temps à partir des entrées suivies. Voici la fonctionnalité:
Fonctionnalité: Suivi du temps Afin de suivre le temps consacré aux tâches En tant qu'employé, je dois gérer une feuille de présence avec des entrées de temps Scénario: Générer une feuille de présence étant donné les entrées de temps suivantes | tâche | durée | | codage | 90 | | codage | 30 | | documenter | 150 | Quand je génère la feuille de temps Ensuite, mon temps total passé à la codification devrait être de 120 minutes Et mon temps total passé à la documentation devrait être de 150 minutes Et mon temps total passé aux réunions devrait être de 0 minute Et le temps total passé à 270 minutes
Rappelez-vous qu'il ne s'agit que d'un exemple. Je trouve plus facile de définir les fonctionnalités dans la vie réelle, car vous avez un problème réel à résoudre et vous avez souvent l'occasion de discuter de la fonctionnalité avec des collègues, des clients ou d'autres parties prenantes..
Bon, demandons à Behat de générer les étapes du scénario pour nous:
$ vendor / bin / behat --dry-run --append-snippets
Nous devons ajuster légèrement les étapes générées. Nous n'avons besoin que de quatre étapes pour couvrir le scénario. Le résultat final devrait ressembler à ceci:
/ ** * @Given J'ai les entrées de temps suivantes * / public function iHaveTheFollowingTimeEntries (TableNode $ table) lance un nouveau PendingException (); / ** * @Lorsque je génère la feuille de temps * / fonction publique iGenerateTheTimeSheet () lance une nouvelle PendingException (); / ** * @Ensuite, le temps total consacré à: tâche devrait être: minute attendu * / fonction publique myTotalTimeSpentOnTaskShouldBeMinutes ($ task, $ attendueDuration) lancer une nouvelle PendingException (); / ** * @Ensuite, le temps total passé devrait être le suivant: minute attendu * / fonction publique myTotalTimeSpentShouldBeMinutes ($ attenduDurée) lancer une nouvelle exception PendingException ();
Notre contexte fonctionnel est prêt à l'emploi, mais nous avons également besoin d'un contexte pour notre suite d'intégration. Tout d’abord, nous allons ajouter la suite au behat.yml
fichier:
défaut: suites: fonctionnel: contextes: [FunctionalFeatureContext] intégration: contextes: [IntegrationFeatureContext]
Ensuite, nous pouvons simplement copier la valeur par défaut FeatureContext
:
$ cp features / bootstrap / FeatureContext.php features / bootstrap / IntegrationFeatureContext.php
N'oubliez pas de changer le nom de la classe en IntegrationFeatureContext
et aussi pour copier la déclaration d'utilisation pour la PendingException
.
Enfin, puisque nous partageons la fonctionnalité, nous pouvons simplement copier les définitions en quatre étapes à partir du contexte fonctionnel. Si vous exécutez Behat, vous verrez que la fonctionnalité est exécutée deux fois: une fois pour chaque contexte..
À ce stade, nous sommes prêts à commencer à remplir les étapes en attente dans notre contexte d'intégration afin de concevoir le domaine principal de notre application. La première étape est Étant donné que j'ai les entrées de temps suivantes
, suivi d'un tableau avec les entrées de temps. Pour rester simple, parcourons simplement les lignes de la table, essayons d'instancier une entrée de temps pour chacune d'elles et ajoutez-les à un tableau d'entrées dans le contexte:
use TimeTracker \ TimeEntry;… / ** * @Given J'ai les entrées de temps suivantes * / fonction publique iHaveTheFollowingTimeEntries (tableNode $ table) $ this-> entrées = []; $ rows = $ table-> getHash (); foreach ($ row as $ row) $ entry = new TimeEntry; $ entry-> task = $ row ['task']; $ entry-> duration = $ row ['duration']; $ ceci-> entrées [] = $ entrée;
Lancer Behat causera une erreur fatale, car le TimeTracker \ TimeEntry
la classe n'existe pas encore. C'est ici que PhpSpec entre en scène. À la fin, TimeEntry
sera une classe éloquente, même si nous ne nous en inquiétons pas encore. PhpSpec et les ORM comme Eloquent ne fonctionnent pas aussi bien ensemble, mais nous pouvons toujours utiliser PhpSpec pour générer la classe et même spécifier un comportement de base. Utilisons les générateurs PhpSpec pour générer le TimeEntry
classe:
$ vendor / bin / phpspec desc "TimeTracker \ TimeEntry" $ vendor / bin / phpspec run Voulez-vous que je crée "TimeTracker \ TimeEntry" pour vous? y
Une fois la classe générée, nous devons mettre à jour la section autoload de notre composer.json
fichier:
"autoload": "classmap": ["app / orders", "app / controllers", "app / models", "app / database / migrations", "app / database / seed"], "psr-4" : "TimeTracker \\": "src / TimeTracker",
Et bien sûr courir composer dump-autoload
.
Courir PhpSpec nous donne le vert. Courir Behat nous donne aussi du vert. Quel bon début!
Laissant Behat guider notre chemin, pourquoi ne pas passer à l'étape suivante?, Quand je génère la feuille de temps
, tout de suite?
Le mot clé ici est "générer", qui ressemble à un terme de notre domaine. Dans le monde des programmeurs, traduire "générer la feuille de temps" en code pourrait simplement vouloir dire instancier une Emploi du temps
classe avec un tas d'entrées de temps. Il est important d'essayer de respecter le langage du domaine lors de la conception de notre code. De cette façon, notre code aidera à décrire le comportement souhaité de notre application..
J'identifie le terme produire
aussi important pour le domaine, c’est pourquoi je pense que nous devrions avoir une méthode de génération statique sur un Emploi du temps
classe qui sert d'alias pour le constructeur. Cette méthode doit prendre une collection d’entrées de temps et les stocker sur la feuille de temps..
Au lieu de simplement utiliser un tableau, je pense qu'il sera logique d'utiliser le Illuminer \ Support \ Collection
classe qui vient avec Laravel. Puisque TimeEntry
sera un modèle Eloquent, lorsque nous interrogerons la base de données pour obtenir les entrées de temps, nous obtiendrons de toute façon une de ces collections Laravel. Que diriez-vous quelque chose comme ça:
utilisez Illuminate \ Support \ Collection; utilisez TimeTracker \ TimeSheet; use TimeTracker \ TimeEntry;… / ** * @When je génère la feuille de temps * / fonction publique iGenerateTheTimeSheet () $ this-> sheet = TimeSheet :: generate (Collection :: make ($ this-> entrées));
À propos, TimeSheet ne sera pas une classe éloquente. Au moins pour le moment, il suffit de faire en sorte que les entrées de temps persistent, puis les feuilles de temps seront simplement généré des entrées.
Lancer Behat, encore une fois, causera une erreur fatale, car Emploi du temps
n'existe pas. PhpSpec peut nous aider à résoudre ce problème:
$ vendor / bin / phpspec desc "TimeTracker \ TimeSheet" $ vendor / bin / phpspec run Voulez-vous que je crée "TimeTracker \ TimeSheet" pour vous? y $ vendor / bin / phpspec lance $ vendor / bin / behat Erreur PHP irrécupérable: appel de la méthode non définie TimeTracker \ TimeSheet :: generate ()
Nous avons toujours une erreur fatale après la création de la classe, parce que la statique produire()
La méthode n'existe toujours pas. Puisqu'il s'agit d'une méthode statique très simple, je ne pense pas qu'il soit nécessaire de spécifier une spécification. Ce n'est rien de plus qu'un wrapper pour le constructeur:
entrées = $ entrées; fonction statique publique generate (Collection $ entrées) retour new static ($ entrées);
Cela ramènera Behat au vert, mais PhpSpec nous lance un petit cri: L'argument 1 transmis à TimeTracker \ TimeSheet :: __ construct () doit être une instance de Illuminate \ Support \ Collection, aucune donnée.
. Nous pouvons résoudre cela en écrivant un simple laisser()
fonction qui sera appelée avant chaque spécification:
put (nouveau TimeEntry); $ this-> beConstructedWith ($ entrées); function it_is_initializable () $ this-> shouldHaveType ('TimeTracker \ TimeSheet');
Cela nous ramènera au vert sur toute la ligne. La fonction s'assure que la feuille de temps est toujours construite avec un modèle de la classe Collection.
Nous pouvons maintenant passer en toute sécurité à la Ensuite, le temps total passé sur…
étape. Nous avons besoin d'une méthode qui prend un nom de tâche et renvoie la durée cumulée de toutes les entrées portant ce nom de tâche. Traduit directement de cornichon en code, cela pourrait être quelque chose comme totalTimeSpentOn ($ tâche)
:
/ ** * @Alors que le temps total consacré à: tâche devrait être: minute attendu * / fonction publique myTotalTimeSpentOnTaskShouldBeMinutes ($ tâche, $ attenduDurée) $ actualDuration = $ ceci-> fiche-> totalTimeSpentOn ($ tâche); PHPUnit :: assertEquals ($ attenduDuration, $ actualDuration);
La méthode n'existe pas, donc lancer Behat nous donnera Appel de la méthode non définie TimeTracker \ TimeSheet :: totalTimeSpentOn ()
.
Afin de spécifier la méthode, nous allons écrire une spécification qui ressemble à ce que nous avons déjà dans notre scénario:
fonction it_should_calculate_total_time_spent_on_task () $ entry1 = new TimeEntry; $ entry1-> task = 'sleep'; $ entry1-> duration = 120; $ entry2 = new TimeEntry; $ entry2-> task = 'eating'; $ entry2-> duration = 60; $ entry3 = new TimeEntry; $ entry3-> task = 'en sommeil'; $ entry3-> duration = 120; $ collection = Collection :: make ([$ entry1, $ entry2, $ entry3]); $ this-> beConstructedWith ($ collection); $ this-> totalTimeSpentOn ('dormir') -> devrait être (240); $ this-> totalTimeSpentOn ('manger') -> devrait être (60);
Notez que nous n’utilisons pas de simulacres pour la TimeEntry
et Collection
les instances. Ceci est notre suite d'intégration et je ne pense pas qu'il soit nécessaire de s'en moquer. Les objets sont assez simples et nous voulons nous assurer que les objets de notre domaine interagissent comme prévu. Il y a probablement de nombreuses opinions à ce sujet, mais cela me semble logique..
Se déplaçant le long:
$ vendor / bin / phpspec run Voulez-vous que je crée 'TimeTracker \ TimeSheet :: totalTimeSpentOn ()' pour vous? y $ vendor / bin / phpspec run 25 devrait calculer le temps total passé sur la tâche attendue [entier: 240], mais a la valeur null.
Afin de filtrer les entrées, nous pouvons utiliser le filtre()
méthode sur le Collection
classe. Une solution simple qui nous fait passer au vert:
fonction publique totalTimeSpentOn ($ tâche) $ entrées = $ ceci-> entrées-> filtre (fonction (entrée $) utiliser ($ tâche) retour $ entrée-> tâche === $ tâche;); $ duration = 0; foreach ($ entrées en tant que $ entry) $ duration + = $ entry-> duration; return $ duration;
Notre cahier des charges est vert, mais j'estime que nous pourrions tirer avantage d'une refactorisation ici. La méthode semble faire deux choses différentes: filtrer les entrées et accumuler la durée. Extrayons ce dernier à sa propre méthode:
fonction publique totalTimeSpentOn ($ tâche) $ entrées = $ ceci-> entrées-> filtre (fonction (entrée $) utiliser ($ tâche) retour $ entrée-> tâche === $ tâche;); return $ this-> sumDuration ($ entrées); fonction protégée sumDuration ($ entrées) $ duration = 0; foreach ($ entrées en tant que $ entry) $ duration + = $ entry-> duration; return $ duration;
PhpSpec est toujours vert et nous avons maintenant trois étapes vertes dans Behat. La dernière étape devrait être facile à mettre en œuvre, car elle est un peu similaire à celle que nous venons de faire..
/ ** * @Ensuite, le temps total passé devrait être le suivant: minute attendue * / fonction publique myTotalTimeSpentShouldBeBeMinutes ($ attenduDurée) $ actualDurée = $ this-> sheet-> totalTimeSpent (); PHPUnit :: assertEquals ($ attenduDuration, $ actualDuration);
Courir Behat nous donnera Appel de la méthode non définie TimeTracker \ TimeSheet :: totalTimeSpent ()
. Au lieu de faire un exemple séparé dans nos spécifications pour cette méthode, pourquoi ne pas l'ajouter à celle que nous avons déjà? Ce n'est peut-être pas tout à fait conforme à ce qui est "juste" de faire, mais soyons un peu pragmatique:
… $ This-> beConstructedWith ($ collection); $ this-> totalTimeSpentOn ('dormir') -> devrait être (240); $ this-> totalTimeSpentOn ('manger') -> devrait être (60); $ this-> totalTimeSpent () -> shouldBe (300);
Laisser PhpSpec générer la méthode:
$ vendor / bin / phpspec run Voulez-vous que je crée 'TimeTracker \ TimeSheet :: totalTimeSpent ()' pour vous? y $ vendor / bin / phpspec run 25 il devrait calculer le temps total passé sur la tâche attendue [entier: 300], mais a la valeur null.
Se rendre au vert est facile maintenant que nous avons le sumDuration ()
méthode:
fonction publique totalTimeSpent () return $ this-> sumDuration ($ this-> entrées);
Et maintenant, nous avons une fonctionnalité verte. Notre domaine évolue lentement!
Nous passons maintenant à notre suite fonctionnelle. Nous allons concevoir l'interface utilisateur et traiter tous les problèmes spécifiques à Laravel qui ne concernent pas notre domaine..
Tout en travaillant dans la suite fonctionnelle, nous pouvons ajouter le -s
drapeau pour indiquer à Behat d’exécuter nos fonctionnalités uniquement à travers le FunctionalFeatureContext
:
$ vendor / bin / behat -s fonctionnel
La première étape ressemblera à la première du contexte d'intégration. Au lieu de simplement faire en sorte que les entrées persistent dans le contexte d'un tableau, nous devons les faire persister dans une base de données afin qu'elles puissent être récupérées plus tard:
use TimeTracker \ TimeEntry;… / ** * @Given J'ai les entrées de temps suivantes * / fonction publique iHaveTheFollowingTimeEntries (TableNode $ table) $ rows = $ table-> getHash (); foreach ($ row as $ row) $ entry = new TimeEntry; $ entry-> task = $ row ['task']; $ entry-> duration = $ row ['duration']; $ entry-> save ();
Courir Behat nous donnera une erreur fatale Appel de la méthode non définie TimeTracker \ TimeEntry :: save ()
, puisque TimeEntry
n'est toujours pas un modèle éloquent. C'est facile à corriger:
espace de noms TimeTracker; la classe TimeEntry s \ 'étend \ Eloquent
Si nous relançons Behat, Laravel se plaindra de ne pas pouvoir se connecter à la base de données. Nous pouvons résoudre ce problème en ajoutant un database.php
déposer au app / config / testing
répertoire, afin d’ajouter les détails de connexion pour notre base de données. Pour les projets plus importants, vous souhaiterez probablement utiliser le même serveur de base de données pour vos tests et votre base de code de production, mais dans notre cas, nous utiliserons simplement une base de données SQLite en mémoire. C'est super simple à configurer avec Laravel:
'sqlite', 'connections' => array ('sqlite' => array ('driver' => 'sqlite', 'database' => ': memory:', 'prefix' => ",),),) ;
Maintenant, si nous courons Behat, il nous dira qu'il n'y a pas entrées_heures
table. Pour résoudre ce problème, nous devons effectuer une migration:
$ php artisan migrate: make createTimeEntriesTable --create = "time_entries"
Schema :: create ('time_entries', fonction (Blueprint $ table) $ table-> incrémente ('id'); $ table-> string ('tâche'); $ table-> entier ('durée'); $ table-> horodatages (););
Nous ne sommes toujours pas verts, car nous avons besoin d'un moyen de charger Behat de gérer nos migrations avant chaque scénario. Nous avons donc une table rase à chaque fois. En utilisant les annotations de Behat, nous pouvons ajouter ces deux méthodes à la LaravelTrait
trait:
/ ** * @BeforeScenario * / public function setupDatabase () $ this-> app ['artisan'] -> call ('migrate'); / ** * @AfterScenario * / fonction publique cleanDatabase () $ this-> app ['artisan'] -> call ('migrate: reset');
Ceci est assez soigné et obtient notre premier pas vers le vert.
La prochaine étape est le Quand je génère la feuille de temps
étape. Selon moi, générer la feuille de temps équivaut à visiter le indice
action de la ressource de saisie de temps, puisque la feuille de temps est la collection de toutes les entrées de temps. Ainsi, l’objet feuille de temps est comme un conteneur pour toutes les entrées de temps et nous offre un moyen agréable de gérer les entrées. Au lieu d'aller à / entrées de temps
, afin de voir la feuille de temps, je pense que l'employé devrait aller à /emploi du temps
. Nous devrions mettre cela dans notre définition de pas:
/ ** * @Quand je génère la feuille de temps * / fonction publique iGenerateTheTimeSheet () $ this-> call ('GET', '/ time-sheet'); $ this-> crawler = new Crawler ($ this-> client-> getResponse () -> getContent (), url ('/'));
Cela provoquera une NotFoundHttpException
, puisque l'itinéraire n'est pas encore défini. Comme je viens de l'expliquer, je pense que cette URL devrait correspondre à la indice
action sur la ressource de saisie de temps:
Route :: get ('time-sheet', ['as' => 'time_sheet', 'uses' => 'TimeEntriesController @ index']);
Pour passer au vert, nous devons générer le contrôleur:
$ php artisan controller: make TimeEntriesController $ composer dump-autoload
Et c'est parti.
Enfin, nous devons explorer la page pour trouver la durée totale des entrées de temps. Je pense que nous aurons une sorte de tableau résumant les durées. Les deux dernières étapes sont si similaires que nous allons simplement les mettre en œuvre en même temps:
/ ** * @Ensuite, le temps total consacré à: tâche devrait être: minute attendu * / fonction publique myTotalTimeSpentOnTaskShouldBeMinutes ($ tâche, $ attenduDurée) $ actualDuration = $ ceci-> analyseur-> filtre ('td #'. $ Tâche . 'TotalDuration') -> text (); PHPUnit :: assertEquals ($ attenduDuration, $ actualDuration); / ** * @Ensuite, le temps total passé devrait être le suivant: minute attendu * / fonction publique myTotalTimeSpentShouldBeMinutes ($ attenduDurée) $ actualDuration = $ ceci-> analyseur-> filtre ('td # totalDuration') -> text (); PHPUnit :: assertEquals ($ attenduDuration, $ actualDuration);
Le robot recherche un Comme nous n’avons toujours pas de vue, le robot nous dira que Afin de résoudre ce problème, construisons le La vue, pour l'instant, va simplement consister en un tableau simple avec les valeurs de durée récapitulées: Si vous exécutez à nouveau Behat, vous constaterez que nous avons implémenté la fonctionnalité avec succès. Nous devrions peut-être prendre un moment pour nous rendre compte que nous n’avons même pas ouvert un navigateur! Il s’agit d’une amélioration considérable de notre flux de travail. En outre, nous avons maintenant des tests automatisés pour notre application. Yay! Si vous courez Si vous exécutez ces deux commandes: Vous verrez que nous sommes revenus au vert, à la fois avec Behat et PhpSpec. Yay! Nous avons maintenant décrit et conçu notre première fonctionnalité, utilisant entièrement une approche BDD. Nous avons vu comment nous pouvons tirer parti de la conception du domaine principal de notre application avant de nous préoccuper de l'interface utilisateur et des éléments spécifiques au framework. Nous avons également vu à quel point il est facile d’interagir avec Laravel, et en particulier avec la base de données, dans nos contextes Behat.. Dans le prochain article, nous allons faire beaucoup de refactorisation afin d’éviter trop de logique sur nos modèles Eloquent, ceux-ci étant plus difficiles à tester isolément et étroitement liés à Laravel. Restez à l'écoute! noeud avec un identifiant de [nom_tâche] TotalDuration
ou durée totale
dans le dernier exemple.La liste de noeuds actuelle est vide.
indice
action. Tout d'abord, nous allons chercher la collection d'entrées de temps. Deuxièmement, nous générons une feuille de temps à partir des entrées et l'envoyons avec la vue (toujours non existante).utilisez TimeTracker \ TimeSheet; utilisez TimeTracker \ TimeEntry; class TimeEntriesController extend \ BaseController / ** * Affiche une liste de la ressource. * * @return Response * / index de fonction publique () $ entries = TimeEntry :: all (); $ sheet = TimeSheet :: generate ($ entrées); retourne View :: make ('time_entries.index', compact ('sheet')); …
Emploi du temps
Tâche Durée totale codage $ sheet-> totalTimeSpentOn ('coding') documenter $ sheet-> totalTimeSpentOn ('documenting') des réunions $ sheet-> totalTimeSpentOn ('meetings') Total $ sheet-> totalTimeSpent () Conclusion
fournisseur / bin / behat
pour pouvoir exécuter les deux suites Behat, vous verrez qu'elles sont toutes les deux vertes maintenant. Si vous utilisez PhpSpec, malheureusement, vous verrez que nos spécifications sont brisées. Nous avons une erreur fatale Classe 'Eloquent' introuvable dans…
. En effet, Eloquent est un alias. Si vous jetez un oeil dans app / config / app.php
sous des alias, vous verrez que Éloquent
est en fait un alias pour Illuminate \ Database \ Eloquent \ Model
. Afin de ramener PhpSpec au vert, nous devons importer cette classe:espace de noms TimeTracker; utilisez Illuminate \ Database \ Eloquent \ Model comme Eloquent; la classe TimeEntry étend Eloquent
$ vendor / bin / phpspec run; fournisseur / bin / behat