() translation by (you can also view the original English article)
Im zweiten Teil dieser Reihe mit den Namen Laravel, BDD und You werden wir beginnen, unser erstes Feature mit Behat und PhpSpec zu beschreiben und zu erstellen. Im letzten Artikel haben wir alles eingerichtet und gesehen, wie einfach wir in unseren Behat-Szenarien mit Laravel interagieren können.
Kürzlich hat der Schöpfer von Behat, Konstantin Kudryashov (a.k.a. everzet), einen wirklich großartigen Artikel mit dem Titel Introducing Modeling by Example geschrieben. Der Workflow, den wir beim Erstellen unserer Funktion verwenden werden, ist stark von dem von everzet vorgestellten inspiriert.
Kurz gesagt, wir werden dieselbe .feature
verwenden, um sowohl unsere Kerndomäne als auch unsere Benutzeroberfläche zu entwerfen. Ich hatte oft das Gefühl, dass meine Funktionen in meinen Akzeptanz- / Funktions- und Integrationssuiten stark dupliziert wurden. Als ich den Vorschlag von everzet las, dasselbe Feature für mehrere Kontexte zu verwenden, hat alles für mich geklickt und ich glaube, es ist der richtige Weg.
In unserem Fall haben wir unseren funktionalen Kontext, der vorerst auch als Akzeptanzschicht dient, und unseren Integrationskontext, der unsere Domäne abdeckt. Wir beginnen mit dem Aufbau der Domain und fügen anschließend die Benutzeroberfläche und die Framework-spezifischen Dinge hinzu.
Kleine Refactorings
Um den Ansatz "Gemeinsame Funktion, mehrere Kontexte" verwenden zu können, müssen wir einige Umgestaltungen unseres vorhandenen Setups vornehmen.
Zunächst werden wir die Begrüßungsfunktion löschen, die wir im ersten Teil erstellt haben, da wir sie nicht wirklich benötigen und sie nicht wirklich dem generischen Stil folgt, den wir benötigen, um mehrere Kontexte zu verwenden.
1 |
$ git rm features/functional/welcome.feature |
Zweitens werden wir unsere Features im Stammverzeichnis des features
-Ordners haben, damit wir das path
attribut aus unserer Datei behat.yml
entfernen können. Wir werden auch den LaravelFeatureContext
in FunctionalFeatureContext
umbenennen (denken Sie daran, auch den Klassennamen zu ändern):
1 |
default: |
2 |
suites: |
3 |
functional: |
4 |
contexts: [ FunctionalFeatureContext ] |
Schließlich, nur um die Dinge ein wenig aufzuräumen, denke ich, wir sollten alle Laravel-bezogenen Dinge in ihre eigene Eigenschaft bringen:
1 |
# features/bootstrap/LaravelTrait.php |
2 |
|
3 |
<?php
|
4 |
|
5 |
use Illuminate\Foundation\Testing\ApplicationTrait; |
6 |
|
7 |
trait LaravelTrait |
8 |
{
|
9 |
/**
|
10 |
* Responsible for providing a Laravel app instance.
|
11 |
*/
|
12 |
use ApplicationTrait; |
13 |
|
14 |
/**
|
15 |
* @BeforeScenario
|
16 |
*/
|
17 |
public function setUp() |
18 |
{
|
19 |
if ( ! $this->app) |
20 |
{
|
21 |
$this->refreshApplication(); |
22 |
}
|
23 |
}
|
24 |
|
25 |
/**
|
26 |
* Creates the application.
|
27 |
*
|
28 |
* @return \Symfony\Component\HttpKernel\HttpKernelInterface
|
29 |
*/
|
30 |
public function createApplication() |
31 |
{
|
32 |
$unitTesting = true; |
33 |
|
34 |
$testEnvironment = 'testing'; |
35 |
|
36 |
return require __DIR__.'/../../bootstrap/start.php'; |
37 |
}
|
38 |
}
|
39 |
Im FunctionalFeatureContext
können wir dann das Merkmal verwenden und die Dinge löschen, die wir gerade verschoben haben:
1 |
/**
|
2 |
* Behat context class.
|
3 |
*/
|
4 |
class FunctionalFeatureContext implements SnippetAcceptingContext |
5 |
{
|
6 |
use LaravelTrait; |
7 |
|
8 |
/**
|
9 |
* Initializes context.
|
10 |
*
|
11 |
* Every scenario gets its own context object.
|
12 |
* You can also pass arbitrary arguments to the context constructor through behat.yml.
|
13 |
*/
|
14 |
public function __construct() |
15 |
{
|
16 |
}
|
Eigenschaften sind eine großartige Möglichkeit, Ihre Kontexte zu bereinigen.
Teilen einer Funktion
Wie in Teil 1 dargestellt, werden wir eine kleine Anwendung für die Zeiterfassung erstellen. Die erste Funktion besteht darin, die Zeit zu verfolgen und aus den verfolgten Einträgen ein Arbeitszeitblatt zu erstellen. Hier ist die Funktion:
1 |
Feature: Tracking time |
2 |
In order to track time spent on tasks |
3 |
As an employee |
4 |
I need to manage a time sheet with time entries |
5 |
|
6 |
Scenario: Generating time sheet |
7 |
Given I have the following time entries |
8 |
| task | duration | |
9 |
| coding | 90 | |
10 |
| coding | 30 | |
11 |
| documenting | 150 | |
12 |
When I generate the time sheet |
13 |
Then my total time spent on coding should be 120 minutes |
14 |
And my total time spent on documenting should be 150 minutes |
15 |
And my total time spent on meetings should be 0 minutes |
16 |
And my total time spent should be 270 minutes |
Denken Sie daran, dass dies nur ein Beispiel ist. Ich finde es einfacher, Features im wirklichen Leben zu definieren, da Sie ein tatsächlich zu lösendes Problem haben und häufig die Möglichkeit haben, das Feature mit Kollegen, Kunden oder anderen Stakeholdern zu diskutieren.
Okay, lassen Sie Behat die Szenario-Schritte für uns generieren:
1 |
$ vendor/bin/behat --dry-run --append-snippets |
Wir müssen die generierten Schritte nur ein kleines bisschen optimieren. Wir brauchen nur vier Schritte, um das Szenario abzudecken. Das Endergebnis sollte ungefähr so aussehen:
1 |
/**
|
2 |
* @Given I have the following time entries
|
3 |
*/
|
4 |
public function iHaveTheFollowingTimeEntries(TableNode $table) |
5 |
{
|
6 |
throw new PendingException(); |
7 |
}
|
8 |
|
9 |
/**
|
10 |
* @When I generate the time sheet
|
11 |
*/
|
12 |
public function iGenerateTheTimeSheet() |
13 |
{
|
14 |
throw new PendingException(); |
15 |
}
|
16 |
|
17 |
/**
|
18 |
* @Then my total time spent on :task should be :expectedDuration minutes
|
19 |
*/
|
20 |
public function myTotalTimeSpentOnTaskShouldBeMinutes($task, $expectedDuration) |
21 |
{
|
22 |
throw new PendingException(); |
23 |
}
|
24 |
|
25 |
/**
|
26 |
* @Then my total time spent should be :expectedDuration minutes
|
27 |
*/
|
28 |
public function myTotalTimeSpentShouldBeMinutes($expectedDuration) |
29 |
{
|
30 |
throw new PendingException(); |
31 |
}
|
Unser funktionaler Kontext ist jetzt einsatzbereit, aber wir benötigen auch einen Kontext für unsere Integrationssuite. Zuerst fügen wir die Suite der Datei behat.yml
hinzu:
1 |
default: |
2 |
suites: |
3 |
functional: |
4 |
contexts: [ FunctionalFeatureContext ] |
5 |
integration: |
6 |
contexts: [ IntegrationFeatureContext ] |
Als nächstes können wir einfach den Standard-FeatureContext
kopieren:
1 |
$ cp features/bootstrap/FeatureContext.php features/bootstrap/IntegrationFeatureContext.php |
Denken Sie daran, den Klassennamen in IntegrationFeatureContext
zu ändern und die use-Anweisung für die PendingException
zu kopieren.
Da wir die Funktion gemeinsam nutzen, können wir die vier Schrittdefinitionen einfach aus dem Funktionskontext kopieren. Wenn Sie Behat ausführen, werden Sie feststellen, dass die Funktion zweimal ausgeführt wird: einmal für jeden Kontext.
Entwerfen der Domain
An diesem Punkt sind wir bereit, die ausstehenden Schritte in unserem Integrationskontext auszufüllen, um die Kerndomäne unserer Anwendung zu entwerfen. Der erste Schritt ist Gegeben, ich habe die folgenden Zeiteinträge, gefolgt
von einer Tabelle mit Zeiteintragsdatensätzen. Um es einfach zu halten, lassen Sie uns einfach die Zeilen der Tabelle durchlaufen, versuchen, einen Zeiteintrag für jeden von ihnen zu instanziieren und sie einem Eintragsarray im Kontext hinzuzufügen:
1 |
use TimeTracker\TimeEntry; |
2 |
|
3 |
...
|
4 |
|
5 |
/**
|
6 |
* @Given I have the following time entries
|
7 |
*/
|
8 |
public function iHaveTheFollowingTimeEntries(TableNode $table) |
9 |
{
|
10 |
$this->entries = []; |
11 |
|
12 |
$rows = $table->getHash(); |
13 |
|
14 |
foreach ($rows as $row) { |
15 |
$entry = new TimeEntry; |
16 |
|
17 |
$entry->task = $row['task']; |
18 |
$entry->duration = $row['duration']; |
19 |
|
20 |
$this->entries[] = $entry; |
21 |
}
|
22 |
}
|
Das Ausführen von Behat führt zu einem schwerwiegenden Fehler, da die TimeTracker\TimeEntry
-Klasse noch nicht vorhanden ist. Hier betritt PhpSpec die Bühne. Am Ende wird TimeEntry
eine eloquente Klasse sein, auch wenn wir uns noch keine Sorgen machen. PhpSpec und ORMs wie Eloquent spielen nicht so gut zusammen, aber wir können trotzdem PhpSpec verwenden, um die Klasse zu generieren und sogar ein grundlegendes Verhalten festzulegen. Verwenden wir die PhpSpec-Generatoren, um die TimeEntry
-Klasse zu generieren:
1 |
$ vendor/bin/phpspec desc "TimeTracker\TimeEntry" |
2 |
$ vendor/bin/phpspec run
|
3 |
Do you want me to create `TimeTracker\TimeEntry` for you? y |
Nachdem die Klasse generiert wurde, müssen wir den Abschnitt zum automatischen Laden unserer Datei composer.json
aktualisieren:
1 |
"autoload": { |
2 |
"classmap": [ |
3 |
"app/commands", |
4 |
"app/controllers", |
5 |
"app/models", |
6 |
"app/database/migrations", |
7 |
"app/database/seeds" |
8 |
],
|
9 |
"psr-4": { |
10 |
"TimeTracker\\": "src/TimeTracker" |
11 |
}
|
12 |
},
|
Und natürlich composer dump-autoload
ausführen.
Das Ausführen von PhpSpec gibt uns grün. Running Behat gibt uns auch grün. Was für ein toller Start!
Lassen Sie sich von Behat leiten, wie wäre es, wenn wir sofort zum nächsten Schritt übergehen: Wenn ich das Arbeitszeitblatt erstelle?
Das Schlüsselwort hier ist "generieren", was wie ein Begriff aus unserer Domain aussieht. In der Welt eines Programmierers kann die Übersetzung von "Arbeitszeittabelle generieren" in Code bedeuten, dass eine Arbeitszeittabellenklasse mit einer Reihe von TimeSheet
instanziiert wird. Es ist wichtig zu versuchen, sich an die Sprache der Domain zu halten, wenn wir unseren Code entwerfen. Auf diese Weise hilft unser Code dabei, das beabsichtigte Verhalten unserer Anwendung zu beschreiben.
Ich identifiziere den Begriff generate
als wichtig für die Domäne, weshalb ich denke, dass wir eine statische Generierungsmethode für eine TimeSheet
-Klasse haben sollten, die als Alias für den Konstruktor dient. Diese Methode sollte eine Sammlung von Zeiteinträgen aufnehmen und auf dem Arbeitszeitblatt speichern.
Anstatt nur ein Array zu verwenden, ist es meiner Meinung nach sinnvoll, die mit Laravel gelieferte Klasse Illuminate\Support\Collection
zu verwenden. Da TimeEntry
ein eloquentes Modell sein wird, erhalten wir bei der Abfrage der Datenbank nach Zeiteinträgen ohnehin eine dieser Laravel-Sammlungen. Wie wäre es mit so etwas:
1 |
use Illuminate\Support\Collection; |
2 |
use TimeTracker\TimeSheet; |
3 |
use TimeTracker\TimeEntry; |
4 |
|
5 |
...
|
6 |
|
7 |
/**
|
8 |
* @When I generate the time sheet
|
9 |
*/
|
10 |
public function iGenerateTheTimeSheet() |
11 |
{
|
12 |
$this->sheet = TimeSheet::generate(Collection::make($this->entries)); |
13 |
}
|
TimeSheet wird übrigens keine eloquente Klasse sein. Zumindest für den Moment müssen wir nur dafür sorgen, dass die Zeiteinträge bestehen bleiben, und dann werden die Arbeitszeitnachweise nur aus den Einträgen generiert.
Das Ausführen von Behat führt erneut zu einem schwerwiegenden Fehler, da TimeSheet
nicht vorhanden ist. PhpSpec kann uns dabei helfen, Folgendes zu lösen:
1 |
$ vendor/bin/phpspec desc "TimeTracker\TimeSheet" |
2 |
$ vendor/bin/phpspec run
|
3 |
Do you want me to create `TimeTracker\TimeSheet` for you? y |
4 |
$ vendor/bin/phpspec run
|
5 |
|
6 |
$ vendor/bin/behat
|
7 |
|
8 |
PHP Fatal error: Call to undefined method TimeTracker\TimeSheet::generate() |
Nach dem Erstellen der Klasse wird immer noch ein schwerwiegender Fehler angezeigt, da die statische Methode generate()
immer noch nicht vorhanden ist. Da dies eine wirklich einfache statische Methode ist, glaube ich nicht, dass eine Spezifikation erforderlich ist. Es ist nichts weiter als ein Wrapper für den Konstruktor:
1 |
<?php
|
2 |
|
3 |
namespace TimeTracker; |
4 |
|
5 |
use Illuminate\Support\Collection; |
6 |
|
7 |
class TimeSheet |
8 |
{
|
9 |
protected $entries; |
10 |
|
11 |
public function __construct(Collection $entries) |
12 |
{
|
13 |
$this->entries = $entries; |
14 |
}
|
15 |
|
16 |
public static function generate(Collection $entries) |
17 |
{
|
18 |
return new static($entries); |
19 |
}
|
20 |
}
|
Dadurch wird Behat wieder grün, aber PhpSpec quietscht uns jetzt an und sagt: Argument 1, das an TimeTracker\TimeSheet::__construct() übergeben wurde, muss eine Instanz von Illuminate\Support\Collection sein, keine Angabe
. Wir können dies lösen, indem wir eine einfache let()
-Funktion schreiben, die vor jeder Spezifikation aufgerufen wird:
1 |
<?php
|
2 |
|
3 |
namespace spec\TimeTracker; |
4 |
|
5 |
use PhpSpec\ObjectBehavior; |
6 |
use Prophecy\Argument; |
7 |
|
8 |
use Illuminate\Support\Collection; |
9 |
use TimeTracker\TimeEntry; |
10 |
|
11 |
class TimeSheetSpec extends ObjectBehavior |
12 |
{
|
13 |
function let(Collection $entries) |
14 |
{
|
15 |
$entries->put(new TimeEntry); |
16 |
|
17 |
$this->beConstructedWith($entries); |
18 |
}
|
19 |
|
20 |
function it_is_initializable() |
21 |
{
|
22 |
$this->shouldHaveType('TimeTracker\TimeSheet'); |
23 |
}
|
24 |
}
|
Dadurch werden wir auf der ganzen Linie wieder grün. Die Funktion stellt sicher, dass das Arbeitszeitblatt immer mit einem Modell der Collection-Klasse erstellt wird.
Wir können jetzt sicher zum Schritt Dann meine gesamte Zeit für...
übergehen. Wir benötigen eine Methode, die einen Aufgabennamen verwendet und die akkumulierte Dauer aller Einträge mit diesem Aufgabennamen zurückgibt. Direkt von Gurke in Code übersetzt, könnte dies so etwas wie totalTimeSpentOn($task)
sein:
1 |
/**
|
2 |
* @Then my total time spent on :task should be :expectedDuration minutes
|
3 |
*/
|
4 |
public function myTotalTimeSpentOnTaskShouldBeMinutes($task, $expectedDuration) |
5 |
{
|
6 |
$actualDuration = $this->sheet->totalTimeSpentOn($task); |
7 |
|
8 |
PHPUnit::assertEquals($expectedDuration, $actualDuration); |
9 |
}
|
Die Methode existiert nicht, daher führt das Ausführen von Behat zum Aufruf der undefinierten Methode TimeTracker\TimeSheet::totalTimeSpentOn()
.
Um die Methode zu spezifizieren, werden wir eine Spezifikation schreiben, die irgendwie ähnlich aussieht wie das, was wir bereits in unserem Szenario haben:
1 |
function it_should_calculate_total_time_spent_on_task() |
2 |
{
|
3 |
$entry1 = new TimeEntry; |
4 |
$entry1->task = 'sleeping'; |
5 |
$entry1->duration = 120; |
6 |
|
7 |
$entry2 = new TimeEntry; |
8 |
$entry2->task = 'eating'; |
9 |
$entry2->duration = 60; |
10 |
|
11 |
$entry3 = new TimeEntry; |
12 |
$entry3->task = 'sleeping'; |
13 |
$entry3->duration = 120; |
14 |
|
15 |
$collection = Collection::make([$entry1, $entry2, $entry3]); |
16 |
|
17 |
$this->beConstructedWith($collection); |
18 |
|
19 |
$this->totalTimeSpentOn('sleeping')->shouldBe(240); |
20 |
$this->totalTimeSpentOn('eating')->shouldBe(60); |
21 |
}
|
Beachten Sie, dass wir für die Instanzen TimeEntry
und Collection
keine Mocks verwenden. Dies ist unsere Integrationssuite, und ich glaube nicht, dass es notwendig ist, dies zu verspotten. Die Objekte sind recht einfach und wir möchten sicherstellen, dass die Objekte in unserer Domäne so interagieren, wie wir es erwarten. Es gibt wahrscheinlich viele Meinungen dazu, aber das macht für mich Sinn.
Weiter gehen:
1 |
$ vendor/bin/phpspec run
|
2 |
Do you want me to create `TimeTracker\TimeSheet::totalTimeSpentOn()` for you? y |
3 |
|
4 |
$ vendor/bin/phpspec run
|
5 |
|
6 |
25 ✘ it should calculate total time spent on task
|
7 |
expected [integer:240], but got null.
|
Um die Einträge zu filtern, können wir die filter()
-Methode für die Collection
-Klasse verwenden. Eine einfache Lösung, die uns zum Grün bringt:
1 |
public function totalTimeSpentOn($task) |
2 |
{
|
3 |
$entries = $this->entries->filter(function($entry) use ($task) |
4 |
{
|
5 |
return $entry->task === $task; |
6 |
});
|
7 |
|
8 |
$duration = 0; |
9 |
|
10 |
foreach ($entries as $entry) { |
11 |
$duration += $entry->duration; |
12 |
}
|
13 |
|
14 |
return $duration; |
15 |
}
|
Unsere Spezifikation ist grün, aber ich bin der Meinung, dass wir hier von einigen Umgestaltungen profitieren könnten. Die Methode scheint zwei verschiedene Dinge zu tun: Einträge filtern und die Dauer akkumulieren. Lassen Sie uns letzteres nach seiner eigenen Methode extrahieren:
1 |
public function totalTimeSpentOn($task) |
2 |
{
|
3 |
$entries = $this->entries->filter(function($entry) use ($task) |
4 |
{
|
5 |
return $entry->task === $task; |
6 |
});
|
7 |
|
8 |
return $this->sumDuration($entries); |
9 |
}
|
10 |
|
11 |
protected function sumDuration($entries) |
12 |
{
|
13 |
$duration = 0; |
14 |
|
15 |
foreach ($entries as $entry) { |
16 |
$duration += $entry->duration; |
17 |
}
|
18 |
|
19 |
return $duration; |
20 |
}
|
PhpSpec ist immer noch grün und wir haben jetzt drei grüne Schritte in Behat. Der letzte Schritt sollte einfach zu implementieren sein, da er dem gerade durchgeführten Schritt etwas ähnlich ist.
1 |
/**
|
2 |
* @Then my total time spent should be :expectedDuration minutes
|
3 |
*/
|
4 |
public function myTotalTimeSpentShouldBeMinutes($expectedDuration) |
5 |
{
|
6 |
$actualDuration = $this->sheet->totalTimeSpent(); |
7 |
|
8 |
PHPUnit::assertEquals($expectedDuration, $actualDuration); |
9 |
}
|
Wenn Sie Behat ausführen, rufen Sie die undefinierte Methode TimeTracker\TimeSheet::totalTimeSpent()
auf. Wie wäre es, wenn wir in unserer Spezifikation kein separates Beispiel für diese Methode erstellen und es einfach zu dem hinzufügen, das wir bereits haben? Es stimmt vielleicht nicht ganz mit dem überein, was "richtig" ist, aber lassen Sie uns ein wenig pragmatisch sein:
1 |
...
|
2 |
|
3 |
$this->beConstructedWith($collection); |
4 |
|
5 |
$this->totalTimeSpentOn('sleeping')->shouldBe(240); |
6 |
$this->totalTimeSpentOn('eating')->shouldBe(60); |
7 |
$this->totalTimeSpent()->shouldBe(300); |
Lassen Sie PhpSpec die Methode generieren:
1 |
$ vendor/bin/phpspec run
|
2 |
Do you want me to create `TimeTracker\TimeSheet::totalTimeSpent()` for you? y |
3 |
|
4 |
$ vendor/bin/phpspec run
|
5 |
|
6 |
25 ✘ it should calculate total time spent on task
|
7 |
expected [integer:300], but got null.
|
Mit der sumDuration()
-Methode ist es jetzt einfach, grün zu werden:
1 |
public function totalTimeSpent() |
2 |
{
|
3 |
return $this->sumDuration($this->entries); |
4 |
}
|
Und jetzt haben wir eine grüne Funktion. Unsere Domain entwickelt sich langsam weiter!
Gestaltung der Benutzeroberfläche
Jetzt ziehen wir in unsere Funktionssuite um. Wir werden die Benutzeroberfläche entwerfen und uns mit allen Laravel-spezifischen Dingen befassen, die nicht das Anliegen unserer Domain sind.
Während der Arbeit in der Funktionssuite können wir das Flag -s
hinzufügen, um Behat anzuweisen, unsere Funktionen nur über den FunctionalFeatureContext
auszuführen:
1 |
$ vendor/bin/behat -s functional |
Der erste Schritt sieht ähnlich aus wie der erste im Integrationskontext. Anstatt nur dafür zu sorgen, dass die Einträge im Kontext eines Arrays bestehen bleiben, müssen sie tatsächlich in einer Datenbank bestehen bleiben, damit sie später abgerufen werden können:
1 |
use TimeTracker\TimeEntry; |
2 |
|
3 |
...
|
4 |
|
5 |
/**
|
6 |
* @Given I have the following time entries
|
7 |
*/
|
8 |
public function iHaveTheFollowingTimeEntries(TableNode $table) |
9 |
{
|
10 |
$rows = $table->getHash(); |
11 |
|
12 |
foreach ($rows as $row) { |
13 |
$entry = new TimeEntry; |
14 |
|
15 |
$entry->task = $row['task']; |
16 |
$entry->duration = $row['duration']; |
17 |
|
18 |
$entry->save(); |
19 |
}
|
20 |
}
|
Das Ausführen von Behat führt zu einem schwerwiegenden Fehler. Rufen Sie die undefinierte Methode TimeTracker\TimeEntry::save()
auf, da TimeEntry
immer noch kein eloquentes Modell ist. Das ist leicht zu beheben:
1 |
namespace TimeTracker; |
2 |
|
3 |
class TimeEntry extends \Eloquent |
4 |
{
|
5 |
}
|
Wenn wir Behat erneut ausführen, beschwert sich Laravel, dass keine Verbindung zur Datenbank hergestellt werden kann. Wir können dies beheben, indem wir dem Verzeichnis app/config/testing
eine Datei database.php
hinzufügen, um die Verbindungsdetails für unsere Datenbank hinzuzufügen. Bei größeren Projekten möchten Sie wahrscheinlich denselben Datenbankserver für Ihre Tests und Ihre Produktionscodebasis verwenden. In unserem Fall verwenden wir jedoch nur eine SQLite-Datenbank im Speicher. Dies ist mit Laravel sehr einfach einzurichten:
1 |
<?php
|
2 |
|
3 |
return array( |
4 |
|
5 |
'default' => 'sqlite', |
6 |
|
7 |
'connections' => array( |
8 |
|
9 |
'sqlite' => array( |
10 |
'driver' => 'sqlite', |
11 |
'database' => ':memory:', |
12 |
'prefix' => '', |
13 |
),
|
14 |
|
15 |
),
|
16 |
|
17 |
);
|
Wenn wir nun Behat ausführen, wird uns mitgeteilt, dass es keine time_entries
-Tabelle gibt. Um dies zu beheben, müssen wir eine Migration durchführen:
1 |
$ php artisan migrate:make createTimeEntriesTable --create="time_entries" |
1 |
Schema::create('time_entries', function(Blueprint $table) |
2 |
{
|
3 |
$table->increments('id'); |
4 |
$table->string('task'); |
5 |
$table->integer('duration'); |
6 |
$table->timestamps(); |
7 |
});
|
Wir sind immer noch nicht grün, da wir eine Möglichkeit benötigen, Behat anzuweisen, unsere Migrationen vor jedem Szenario durchzuführen, sodass wir jedes Mal eine saubere Tafel haben. Mithilfe der Anmerkungen von Behat können wir diese beiden Methoden zum Merkmal LaravelTrait
hinzufügen:
1 |
/**
|
2 |
* @BeforeScenario
|
3 |
*/
|
4 |
public function setupDatabase() |
5 |
{
|
6 |
$this->app['artisan']->call('migrate'); |
7 |
}
|
8 |
|
9 |
/**
|
10 |
* @AfterScenario
|
11 |
*/
|
12 |
public function cleanDatabase() |
13 |
{
|
14 |
$this->app['artisan']->call('migrate:reset'); |
15 |
}
|
Das ist ziemlich ordentlich und bringt unseren ersten Schritt zum Grün.
Als nächstes folgt der Schritt Wenn ich das Arbeitszeitblatt generiere
. So wie ich es sehe, entspricht das Generieren des Arbeitszeitblatts dem Aufrufen der index
aktion der Zeiteintragsressource, da das Arbeitszeitblatt die Sammlung aller Zeiteinträge ist. Das Arbeitszeitblattobjekt ist also wie ein Container für alle Zeiteinträge und bietet uns eine gute Möglichkeit, Einträge zu verarbeiten. Anstatt zu /time-entries
zu gehen, sollte der Mitarbeiter, um das Arbeitszeitblatt zu sehen, zu /time-sheet
gehen. Wir sollten das in unsere Schrittdefinition aufnehmen:
1 |
/**
|
2 |
* @When I generate the time sheet
|
3 |
*/
|
4 |
public function iGenerateTheTimeSheet() |
5 |
{
|
6 |
$this->call('GET', '/time-sheet'); |
7 |
|
8 |
$this->crawler = new Crawler($this->client->getResponse()->getContent(), url('/')); |
9 |
}
|
Dies führt zu einer NotFoundHttpException
, da die Route noch nicht definiert ist. Wie ich gerade erklärt habe, sollte diese URL der index
aktion für die Zeiteintragsressource zugeordnet werden:
1 |
Route::get('time-sheet', ['as' => 'time_sheet', 'uses' => 'TimeEntriesController@index']); |
Um grün zu werden, müssen wir den Controller generieren:
1 |
$ php artisan controller:make TimeEntriesController
|
2 |
$ composer dump-autoload
|
Und los geht's.
Schließlich müssen wir die Seite crawlen, um die Gesamtdauer der Zeiteinträge zu ermitteln. Ich gehe davon aus, dass wir eine Art Tabelle haben werden, die die Dauer zusammenfasst. Die letzten beiden Schritte sind so ähnlich, dass wir sie nur gleichzeitig implementieren werden:
1 |
/**
|
2 |
* @Then my total time spent on :task should be :expectedDuration minutes
|
3 |
*/
|
4 |
public function myTotalTimeSpentOnTaskShouldBeMinutes($task, $expectedDuration) |
5 |
{
|
6 |
$actualDuration = $this->crawler->filter('td#' . $task . 'TotalDuration')->text(); |
7 |
|
8 |
PHPUnit::assertEquals($expectedDuration, $actualDuration); |
9 |
}
|
10 |
|
11 |
/**
|
12 |
* @Then my total time spent should be :expectedDuration minutes
|
13 |
*/
|
14 |
public function myTotalTimeSpentShouldBeMinutes($expectedDuration) |
15 |
{
|
16 |
$actualDuration = $this->crawler->filter('td#totalDuration')->text(); |
17 |
|
18 |
PHPUnit::assertEquals($expectedDuration, $actualDuration); |
19 |
}
|
Der Crawler sucht im letzten Beispiel nach einem <td>
-Knoten mit der ID [task_name]TotalDuration
oder totalDuration
.
Da wir immer noch keine Ansicht haben, teilt uns der Crawler mit, dass die aktuelle Knotenliste leer ist
.
Um dies zu beheben, erstellen wir die index
aktion. Zuerst holen wir die Sammlung von Zeiteinträgen. Zweitens generieren wir aus den Einträgen ein Arbeitszeitblatt und senden es an die (noch nicht vorhandene) Ansicht.
1 |
use TimeTracker\TimeSheet; |
2 |
use TimeTracker\TimeEntry; |
3 |
|
4 |
class TimeEntriesController extends \BaseController { |
5 |
|
6 |
/**
|
7 |
* Display a listing of the resource.
|
8 |
*
|
9 |
* @return Response
|
10 |
*/
|
11 |
public function index() |
12 |
{
|
13 |
$entries = TimeEntry::all(); |
14 |
$sheet = TimeSheet::generate($entries); |
15 |
|
16 |
return View::make('time_entries.index', compact('sheet')); |
17 |
}
|
18 |
|
19 |
...
|
20 |
Die Ansicht besteht vorerst nur aus einer einfachen Tabelle mit den zusammengefassten Dauerwerten:
1 |
<h2>Time Sheet</h2> |
2 |
|
3 |
<table>
|
4 |
<thead>
|
5 |
<th>Task</th> |
6 |
<th>Total duration</th> |
7 |
</thead>
|
8 |
<tbody>
|
9 |
<tr>
|
10 |
<td>coding</td> |
11 |
<td id="codingTotalDuration">{{ $sheet->totalTimeSpentOn('coding') }}</td> |
12 |
</tr>
|
13 |
<tr>
|
14 |
<td>documenting</td> |
15 |
<td id="documentingTotalDuration">{{ $sheet->totalTimeSpentOn('documenting') }}</td> |
16 |
</tr>
|
17 |
<tr>
|
18 |
<td>meetings</td> |
19 |
<td id="meetingsTotalDuration">{{ $sheet->totalTimeSpentOn('meetings') }}</td> |
20 |
</tr>
|
21 |
<tr>
|
22 |
<td><strong>Total</strong></td> |
23 |
<td id="totalDuration">{{ $sheet->totalTimeSpent() }}</td> |
24 |
</tr>
|
25 |
</tbody>
|
26 |
</table>
|
Wenn Sie Behat erneut ausführen, werden Sie feststellen, dass wir die Funktion erfolgreich implementiert haben. Vielleicht müssen wir uns einen Moment Zeit nehmen, um festzustellen, dass wir nicht einmal einen Browser geöffnet haben! Das ist eine riesige Verbesserung unseres Workflows. Als netten Bonus haben wir jetzt automatisierte Tests für unsere Anwendung. Cool!
Abschluss
Wenn Sie vendor/bin/behat
ausführen, um beide Behat-Suiten auszuführen, werden Sie feststellen, dass beide jetzt grün sind. Wenn Sie PhpSpec ausführen, werden Sie leider feststellen, dass unsere Spezifikationen fehlerhaft sind. Wir erhalten einen schwerwiegenden Fehler. Klasse 'Eloquent' nicht gefunden in...
. Dies liegt daran, dass Eloquent ein Alias ist. Wenn Sie in app/config/app.php
unter Aliase nachsehen, werden Sie feststellen, dass Eloquent
tatsächlich ein Alias für Illuminate\Database\Eloquent\Model
ist. Um PhpSpec wieder grün zu machen, müssen wir diese Klasse importieren:
1 |
namespace TimeTracker; |
2 |
|
3 |
use Illuminate\Database\Eloquent\Model as Eloquent; |
4 |
|
5 |
class TimeEntry extends Eloquent |
6 |
{
|
7 |
}
|
Wenn Sie diese beiden Befehle ausführen:
1 |
$ vendor/bin/phpspec run; vendor/bin/behat |
Sie werden sehen, dass wir sowohl mit Behat als auch mit PhpSpec wieder grün sind. Cool!
Wir haben jetzt unsere erste Funktion beschrieben und entworfen, die vollständig einen BDD-Ansatz verwendet. Wir haben gesehen, wie wir vom Entwerfen der Kerndomäne unserer Anwendung profitieren können, bevor wir uns um die Benutzeroberfläche und die Framework-spezifischen Dinge kümmern. Wir haben auch gesehen, wie einfach es ist, mit Laravel und insbesondere der Datenbank in unseren Behat-Kontexten zu interagieren.
Im nächsten Artikel werden wir viel umgestalten, um zu viel Logik bei unseren Eloquent-Modellen zu vermeiden, da diese schwieriger isoliert zu testen sind und eng mit Laravel verbunden sind. Bleiben Sie dran!