Indonesian (Bahasa Indonesia) translation by Ari Gustiawan (you can also view the original English article)
Dalam bagian kedua dari seri ini disebut Laravel, BDD dan Anda, kami akan mulai menggambarkan dan membangun fitur kami pertama menggunakan Behat dan PhpSpec. Dalam artikel terakhir kami punya segalanya diatur dan melihat bagaimana dengan mudah kita dapat berinteraksi dengan Laravel dalam skenario Behat.
Baru saja creator Behat, Konstantin Kudryashov (alias everzet), menulis sebuah artikel yang benar-benar hebat disebut Introducing Modelling by Example. Workflow kita akan menggunakan, ketika kita membangun fitur kami, sangat terinspirasi oleh everzet.
Singkatnya, kita akan menggunakan .feature
ke design core kita dan user interface. Saya sering merasa bahwa saya memiliki banyak duplikasi di fitur saya dalam acceptance/functional dan integration suites. Ketika saya membaca saran everzet's tentang cara menggunakan fitur yang sama untuk beberapa konteks, itu semua mengklik bagi saya dan saya percaya itu adalah cara untuk terbaik.
Dalam kasus kami, kita akan memiliki kami functional context, yang, untuk saat ini, juga akan berfungsi sebagai acceptance layer, dan integration context kita, yang akan mencakup domain kita. Kita akan mulai dengan membangun domain dan kemudian tambahkan UI dan hal-hal khusus framework.
Small Refactoring
Untuk menggunakan pendekatan "shared feature, muptiple context", yang harus kita lakukan beberapa refactorings kami setup yang sudah ada.
Pertama, kami akan menghapus fitur welcome yang kita lakukan di bagian pertama, karena kita tidak benar-benar membutuhkannya dan itu tidak benar-benar mengikuti style generik yang kita butuhkan untuk menggunakan beberapa konteks.
1 |
$ git rm features/functional/welcome.feature |
Kedua, kita akan memiliki fitur kami di root folder fitur, sehingga kita dapat pergi ke depan dan menghapus atribut path dari file behat.yml kami. Kami juga akan untuk mengubah nama LaravelFeatureContext FunctionalFeatureContext (ingat untuk mengubah nama kelas juga):
1 |
default: |
2 |
suites: |
3 |
functional: |
4 |
contexts: [ FunctionalFeatureContext ] |
Akhirnya, hanya untuk membersihkan hal-hal sedikit, saya pikir kita harus pindah semua yang berhubungan dengn laravel ke traitnya:
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 |
Dalam FunctionalFeatureContext
kita kemudian dapat menggunakan trait dan menghapus hal-hal yang baru saja kita pindahkan:
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 |
}
|
Trait adalah cara yang bagus untuk membersihkan konteks Anda.
Fitur sharing
Seperti yang disajikan pada bagian satu, kita akan membangun sebuah aplikasi kecil untuk time tracking. Fitur pertama akan tentang time tracking dan generate time sheet dari entri yang dilacak. Berikut adalah fitur:
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 |
Ingatlah bahwa ini adalah hanya sebuah contoh. Saya merasa lebih mudah untuk menentukan fitur dalam kehidupan nyata, karena Anda memiliki masalah yang sebenarnya Anda butuhkan untuk memecahkan dan sering mendapatkan kesempatan untuk membahas fitur dengan rekan-rekan, klien atau stakeholders lainnya.
Oke, mari kita memiliki Behat menghasilkan langkah-langkah skenario untuk kami:
1 |
$ vendor/bin/behat --dry-run --append-snippets |
Kita perlu men-tweak langkah-langkah yang dihasilkan hanya sedikit. Kita hanya perlu empat langkah untuk meng-cover skenario. Hasil akhir akan terlihat seperti ini:
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 |
}
|
Konteks fungsional kami semua sudah siap, tetapi kita juga perlu konteks untuk integration suite. Pertama, kita akan menambahkan suite ke behat.yml
file:
1 |
default: |
2 |
suites: |
3 |
functional: |
4 |
contexts: [ FunctionalFeatureContext ] |
5 |
integration: |
6 |
contexts: [ IntegrationFeatureContext ] |
Selanjutnya, kami hanya dapat menyalin default FeatureContext
:
1 |
$ cp features/bootstrap/FeatureContext.php features/bootstrap/IntegrationFeatureContext.php |
Ingat untuk mengubah nama kelas IntegrationFeatureContext
dan juga untuk menyalin statement yang digunakan untuk PendingException
.
Akhirnya, karena kita berbagi fitur, kita dapat hanya menyalin definisi empat langkah dari functional context. Jika Anda menjalankan Behat, Anda akan melihat bahwa fitur dijalankan dua kali: sekali untuk setiap konteks.
Merancang Domain
Pada point ini, kami siap untuk mulai mengisi langkah tertunda dalam integration context kami untuk desain domain inti dari aplikasi kita. Langkah pertama Given I have the following time entries
, diikuti oleh sebuah tabel dengan catatan catatan waktu. Jaga agar tetap sederhana, mari kita hanya loop atas baris tabel, cobalah untuk instantiate sebuah catatan waktu untuk masing-masing, dan menambahkannya ke array entri pada konteks:
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 |
}
|
Menjalankan Behat akan menyebabkan kesalahan fatal, karena kelas TimeTracker\TimeEntry
belum ada. Ini adalah dimana PhpSpec memasuki tahap. Pada akhirnya, TimeEntry
akan menjadi Eloquent class, meskipun kita belum khawatir tentang hal itu. PhpSpec dan ORMs seperti Eloquent do tidak bermain bersama-sama baik, tetapi kita masih bisa menggunakan PhpSpec untuk menghasilkan kelas dan bahkan spec keluar beberapa basic behavior. Mari kita gunakan PhpSpec Generator untuk generate TimeEntry
class:
1 |
$ vendor/bin/phpspec desc "TimeTracker\TimeEntry" |
2 |
$ vendor/bin/phpspec run
|
3 |
Do you want me to create `TimeTracker\TimeEntry` for you? y |
Setelah kelas ter-generate, kita perlu memperbarui bagian autoload file composer.json
kami:
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 |
},
|
Dan tentu saja jalankn composer dump-autoload
.
Menjalankan PhpSpec memberi kita hijau. Menjalankan Behat memberi kita hijau juga. Apa yang besar untuk memulai!
Membiarkan Behat panduan cara kita, bagaimana kita hanya bergerak tahap berikutnya, When I generate the time sheet
, segera?
Kata kunci di sini adalah "generate", yang terlihat seperti sebuah istilah dari domain kita. Dalam dunia programmer, menerjemahkan "generate the timesheet" kode bisa hanya berarti instantiating kelas TimeSheet
dengan sekelompok waktu entri. Hal ini penting untuk mencoba dan tetap berpegang pada bahasa dari domain ketika kita merancang kode kita. Dengan cara itu, kode kita akan membantu menjelaskan perilaku dimaksudkan aplikasi kita.
Saya mengidentifikasi istilah generate
sebagai penting untuk domain, yang adalah mengapa saya pikir kita harus memiliki statis generate metode kelas TimeSheet
yang berfungsi sebagai sebuah alias untuk konstruktor. Metode ini harus mengambil koleksi waktu entri dan menyimpannya pada waktu lembar.
Daripada hanya menggunakan array, saya pikir itu akan masuk akal untuk menggunakan Illuminate\Support\Collection
kelas yang datang dengan Laravel. Karena TimeEntry
akan menjadi model Eloquent, ketika kita query database untuk entri waktu, kita akan mendapatkan salah satu koleksi Laravel ini pula. Bagaimana tentang sesuatu seperti ini:
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 |
}
|
By the way, TimeSheet tidak akan menjadi kelas Eloquent. Setidaknya untuk saat ini, kita hanya perlu membuat entri bertahan, dan kemudian waktu lembar akan hanya generate dari entri.
Menjalankan Behat akan, sekali lagi, menyebabkan kesalahan fatal, karena TimeSheet
tidak ada. PhpSpec dapat membantu kami memecahkan itu:
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() |
Kita masih mendapatkan kesalahan fatal setelah membuat kelas, karena metode statis generate()
masih belum ada. Karena ini adalah metode statis yang benar-benar sederhana, saya tidak berpikir ada kebutuhan untuk spesifikasi. Hal ini tidak lebih daripada sebuah bungkus untuk 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 |
}
|
Ini akan mendapatkan Behat kembali ke green, tetapi PhpSpec sekarang mendecit pada kami, berkata: Argument 1 passed to TimeTracker\TimeSheet::__construct() must be an instance of Illuminate\Support\Collection, none given.
Kita dapat memecahkan masalah ini dengan menulis fungsi sederhana let()
yang akan dipanggil sebelum setiap spesifikasi:
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 |
}
|
Ini akan mendapatkan kita kembali ke green seluruh baris. Fungsi memastikan bahwa lembar waktu selalu dibangun dengan mock kelas koleksi.
Kami dengan aman sekarang dapat pindah ke Then my total time spent on...
langkah. Kami memerlukan metode yang mengambil nama tugas dan kembali durasi akumulasi semua entri dengan nama tugas ini. Langsung diterjemahkan dari gherkin ke kode, ini bisa menjadi sesuatu seperti totalTimeSpentOn($task):
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 |
}
|
Metode tidak ada, jadi menjalankan Behat akan memberikan Call to undefined method TimeTracker\TimeSheet::totalTimeSpentOn()
Untuk spesifikasi keluar metode, kita akan menulis spesifikasi yang entah bagaimana terlihat mirip dengan apa yang sudah kita miliki dalam skenario kami:
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 |
}
|
Perhatikan bahwa kami tidak menggunakanmocksuntuk contoh-contoh TimeEntry
dan Collection
. Ini adalah integration suite kami dan saya tidak berpikir ada kebutuhan untuk mock hal ini. Objek cukup sederhana dan kami ingin untuk memastikan bahwa objek di domain kita berinteraksi sebagai kita berharap mereka. Ada mungkin banyak pendapat tentang hal ini, tetapi ini masuk akal bagi saya.
Moving along:
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.
|
Untuk menyaring entri, kita dapat menggunakan metode filter()
pada kelas Collection
. Solusi sederhana yang membuat kita menjadi hijau:
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 |
}
|
Spesifikasi kami hijau, tapi aku merasa bahwa kami dapat memperoleh manfaat dari beberapa refactoring di sini. Metode tampaknya untuk melakukan dua hal yang berbeda: menyaring entri dan mengumpulkan durasi. Mari kita ekstrak yang kedua metode sendiri:
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 masih hijau dan kami sekarang memiliki tiga langkah hijau di Behat. Langkah terakhir harus mudah untuk melaksanakan, karena hal ini agak mirip dengan yang kita hanya melakukan.
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 |
}
|
Menjalankan Behat akan memberi Call to undefined method TimeTracker\TimeSheet::totalTimeSpent()
. Daripada melakukan sebuah contoh yang terpisah kami spec untuk metode ini, bagaimana kita hanya menambahkannya ke yang kita sudah memiliki? Itu mungkin tidak akan benar-benar sesuai dengan apa "benar" untuk melakukan, tetapi marilah kita menjadi sedikit pragmatis:
1 |
...
|
2 |
|
3 |
$this->beConstructedWith($collection); |
4 |
|
5 |
$this->totalTimeSpentOn('sleeping')->shouldBe(240); |
6 |
$this->totalTimeSpentOn('eating')->shouldBe(60); |
7 |
$this->totalTimeSpent()->shouldBe(300); |
Biarkan PhpSpec generate metode:
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.
|
Mendapatkan hijau mudah sekarang bahwa kita memiliki sumDuration()
metode:
1 |
public function totalTimeSpent() |
2 |
{
|
3 |
return $this->sumDuration($this->entries); |
4 |
}
|
Dan sekarang kami memiliki fitur hijau. Domain kita perlahan-lahan berkembang!
Merancang User Interface
Sekarang, kita sedang bergerak ke functional suite. Kita akan desain user interface dan menangani semua Laravel-stuff khusus yang tidak diperlukan domain kita.
Saat bekerja di functional suite, kita dapat menambahkan flag -s
memerintahkan Behat untuk hanya menjalankan fitur kami melalui FunctionalFeatureContext
:
1 |
$ vendor/bin/behat -s functional |
Langkah pertama akan terlihat mirip dengan yang pertama dari integration context. Daripada hanya membuat entri bertahan pada konteks dalam array, kita perlu untuk benar-benar membuat mereka bertahan dalam database sehingga mereka dapat diambil kemudian:
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 |
}
|
Menjalankan Behat akan memberi kita kesalahan Call to undefined method TimeTracker\TimeEntry::save(
), karena TimeEntry
masih bukan model Eloquent. Ini mudah untuk memperbaiki:
1 |
namespace TimeTracker; |
2 |
|
3 |
class TimeEntry extends \Eloquent |
4 |
{
|
5 |
}
|
Jika kita menjalankan Behat lagi, Laravel akan mengeluh bahwa ia tidak dapat terhubung ke database. Kita bisa memperbaiki ini dengan menambahkan database.php
file ke direktori app/config/testing
, untuk menambahkan rincian koneksi database. Untuk proyek yang lebih besar, Anda mungkin ingin menggunakan server database yang sama untuk tes dan basis kode produksi Anda, tetapi dalam kasus kami, kami hanya akan menggunakan memori di SQLite database. Ini super sederhana untuk mengatur dengan Laravel:
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 |
);
|
Sekarang jika kita menjalankan Behat, itu akan memberitahu kita bahwa ada tidak ada tabmle time_entries
. Untuk memperbaiki ini, kita perlu membuat migation:
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 |
});
|
Kami masih tidak hijau, karena kita perlu cara untuk mengajar Behat untuk menjalankan migration sebelum setiap skenario, jadi kita memiliki bersih setiap saat. Dengan menggunakan Behat's anotasi, kita dapat menambahkan dua metode ini untuk trait LaravelTrait
:
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 |
}
|
Ini cukup rapi dan mendapat langkah pertama kami menjadi hijau.
Berikutnya adalah langkah When I generate the time sheet
. Cara saya melihatnya, generate lembar waktu adalah setara dengan mengunjungi index
action dari time entry resource, karena time sheet adalah kumpulan dari semua entri waktu. Jadi objek time sheet adalah seperti sebuah wadah untuk semua entri waktu dan memberi kita cara yang baik untuk menangani entri. Bukannya pergi ke /time-entries
, untuk melihat time sheet , saya pikir karyawan harus pergi ke /time-sheet
. Kita harus meletakkan bahwa dalam definisi langkah kita:
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 |
}
|
Ini akan menyebabkan NotFoundHttpException
, karena rute tidak didefinisikan. Aku hanya dijelaskan, saya pikir URL ini harus map index
action pada time entry resource:
1 |
Route::get('time-sheet', ['as' => 'time_sheet', 'uses' => 'TimeEntriesController@index']); |
Untuk mendapatkan hijau, kita perlu menghasilkan controller:
1 |
$ php artisan controller:make TimeEntriesController
|
2 |
$ composer dump-autoload
|
Dan ada kita pergi.
Akhirnya, kita perlu untuk menjelajah halaman untuk menemukan total durasi waktu entri. Saya rasa kita akan memiliki semacam tabel yang merangkum durasi. Terakhir dua langkah sangat mirip bahwa kita hanya akan menerapkan mereka pada saat yang sama:
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 |
}
|
Crawler mencari<td>
node dengan id dari [task_name] TotalDuration
atau totalDuration
dalam contoh terakhir.
Karena kita masih tidak memiliki view, crawler akan memberitahu kita The current node list is empty.
Untuk memperbaiki ini, mari kita membangun index
action. Pertama, kita mengambil koleksi time sheet. Kedua, kami menghasilkan time sheet dari entri dan mengirimkannya ke tampilan (masih tidak ada).
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 |
Tampilan, untuk saat ini, hanya akan terdiri dari sebuah tabel sederhana dengan nilai durasi diringkas:
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>
|
Jika Anda menjalankan Behat lagi, Anda akan melihat bahwa kita berhasil menerapkan fitur. Mungkin kita harus mengambil waktu untuk menyadari bahwa bahkan tidak sekali Apakah kita membuka browser! Ini adalah perbaikan besar-besaran untuk alur kerja kami, dan sebagai bonus bagus, kita sekarang memiliki otomatis tes untuk aplikasi kita. YAY!
Kesimpulan
Jika Anda menjalankan vendor/bin/behat
untuk menjalankan kedua Behat suites, Anda akan melihat bahwa keduanya hijau sekarang. Jika Anda menjalankan PhpSpec meskipun, sayangnya, Anda akan melihat bahwa spesifikasi kami rusak. Kita mendapatkan kesalahan Class 'Eloquent' not found in ....
Hal ini karena Eloquent adalah alias. Jika Anda melihat di app/config/app.php
di bawah alias, Anda akan melihat bahwa Eloquent
adalah benar-benar sebuah alias untuk Illuminate\Database\Eloquent\Model
. Untuk mendapatkan PhpSpec kembali ke green, kita perlu mengimpor kelas ini:
1 |
namespace TimeTracker; |
2 |
|
3 |
use Illuminate\Database\Eloquent\Model as Eloquent; |
4 |
|
5 |
class TimeEntry extends Eloquent |
6 |
{
|
7 |
}
|
Jika Anda menjalankan 2 perintah ini:
1 |
$ vendor/bin/phpspec run; vendor/bin/behat |
Anda akan melihat bahwa kita adalah kembali ke green, baik dengan Behat dan PhpSpec. YAY!
Kami sekarang memiliki dijelaskan dan dirancang fitur pertama kami, benar-benar menggunakan pendekatan BDD. Kita telah melihat bagaimana kita dapat manfaat dari merancang domain inti aplikasi kami, sebelum kita khawatir tentang UI dan hal-hal tertentu framework. Kita juga telah melihat betapa mudahnya untuk berinteraksi dengan Laravel, dan terutama database, dalam konteks Behat kami.
Dalam artikel berikutnya, kami akan melakukan banyak refactoring untuk menghindari terlalu banyak logika pada model Eloquent, karena ini lebih sulit untuk menguji dalam isolasi dan erat digabungkan ke Laravel. Stay tuned!