() translation by (you can also view the original English article)
Testing controller bukan hal yang termudah di dunia. Nah, biarkan aku ulang kata-kata yang: menguji mereka adalah menang; Apa itu sulit, setidaknya pada awalnya, adalah menentukan apa yang akan diuji.
Harus tes controller memverifikasi teks pada halaman? Harus itu menyentuh database? Harus itu memastikan bahwa variabel ada dalam view? Jika ini adalah jperjalanan pertama Anda, hal ini dapat membingungkan! Biarkan saya membantu.
Controller tes harus memverifikasi response, memastikan bahwa metode akses database benar dipicu, dan menegaskan bahwa variabel sesuai contoh dikirim ke view.
Proses pengujian controller dapat dibagi menjadi tiga bagian.
- Isolate: Mock semua dependensi (mungkin termasuk
View
). - Call: Memicu metode controller yang diinginkan.
- Ensure: Melakukan pernyataan, memverifikasi bahwa stage telah ditetapkan dengan benar.
Hello World controller Testing
Cara terbaik untuk belajar hal-hal ini adalah melalui contoh. Berikut ini adalah "hello world" controller pengujian di Laravel.
1 |
<?php
|
2 |
|
3 |
# app/tests/controllers/PostsControllerTest.php
|
4 |
|
5 |
class PostsControllerTest extends TestCase { |
6 |
|
7 |
public function testIndex() |
8 |
{
|
9 |
$this->client->request('GET', 'posts'); |
10 |
}
|
11 |
|
12 |
}
|
Laravel memanfaatkan segenggam Symfony's komponen untuk memudahkan proses pengujian route dan View, termasuk HttpKernel, DomCrawler, dan BrowserKit. Ini adalah mengapa sangat penting bahwa tes PHPUnit Anda mewarisi dari, tidak dari PHPUnit\_Framework\_TestCase
, tapi dari TestCase
. Jangan khawatir, Laravel masih extend itu, tapi hal itu membantu men-setup app Laravel untuk pengujian, serta menyediakan berbagai macam helper metode penegasan bahwa Anda dianjurkan untuk menggunakan. Lebih pada segera.
Dalam potongan kode di atas, kita membuat sebuah GET
permintaan untuk /posts
, atau localhost:8000 /posts
. Dengan asumsi bahwa baris ini ditambahkan ke instalasi baru Laravel, Symfony akan melemparkan NotFoundHttpException
. Jika bekerja bersama, mencobanya dengan menjalankan phpunit
dari baris perintah.
1 |
$ phpunit
|
2 |
1) PostsControllerTest::testIndex
|
3 |
Symfony\Component\HttpKernel\Exception\NotFoundHttpException: |
Dalam human-speak, ini pada dasarnya diterjemahkan menjadi, "Hei, saya mencoba memanggil rute itu, tapi Anda tidak memiliki apa pun terdaftar, bodoh!"
Seperti yang dapat Anda bayangkan, ini jenis permintaan cukup umum ke titik bahwa masuk akal untuk menyediakan metode helper, seperti $this->call()
. Pada kenyataannya, Laravel melakukan hal yang sama! Ini berarti bahwa contoh sebelumnya dapat direfractor, seperti:
1 |
#app/tests/controllers/PostsControllerTest.php
|
2 |
|
3 |
public function testIndex() |
4 |
{
|
5 |
$this->call('GET', 'posts'); |
6 |
}
|
Overloading adalah teman Anda
Meskipun kami akan tetap dengan fungsi dasar dalam bab ini, dalam proyek-proyek pribadi saya, saya mengambil hal-hal satu langkah lebih lanjut oleh memungkinkan untuk metode tersebut sebagai $this->get()
, $this->post(),
dll. Berkat PHP overloading, ini hanya membutuhkan penambahan metode tunggal, yang Anda dapat menambah app/tests/TestCase.php.
1 |
# app/tests/TestCase.php
|
2 |
|
3 |
public function __call($method, $args) |
4 |
{
|
5 |
if (in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) |
6 |
{
|
7 |
return $this->call($method, $args[0]); |
8 |
}
|
9 |
|
10 |
throw new BadMethodCallException; |
11 |
}
|
Sekarang, Anda bebas untuk menulis $this->get('posts')
dan mencapai hasil yang sama sebagai dua contoh sebelumnya. Seperti disebutkan di atas, namun, mari kita tetap dengan fungsi dasar framework untuk lebih mudahnya.
Untuk membuat lulus tes, kita hanya perlu mempersiapkan route yang tepat.
1 |
<?php
|
2 |
|
3 |
# app/routes.php
|
4 |
|
5 |
Route::get('posts', function() |
6 |
{
|
7 |
return 'all posts'; |
8 |
});
|
Menjalankan phpunit
lagi akan me-return green.
Laravel Helper Assertion
Tes yang Anda akan menemukan diri menulis berulang kali adalah salah satu yang menjamin bahwa kontroler melewati sebuah variabel tertentu ke view. Sebagai contoh, metode index
dari PostsController
harus pass variabel $posts
ke view yang terkait, benar? Dengan cara itu, tampilan dapat menyaring melalui semua posting, dan menampilkannya pada halaman. Ini adalah merupakan tes untuk ditulis!
Jika itu task yang umum, kemudian, sekali lagi, tidak masuk akal untuk Laravel untuk memberikan helper assertion untuk mencapai hal ini? Tentu saja itu akan. Dan, tentu saja, Laravel juga!
Illuminate\Foundation\Testing\TestCase
termasuk sejumlah metode yang secara drastis akan mengurangi jumlah kode yang diperlukan untuk melakukan pernyataan-pernyataan dasar. Daftar ini mencakup:
assertViewHas
assertResponseOk
assertRedirectedTo
assertRedirectedToRoute
assertRedirectedToAction
assertSessionHas
assertSessionHasErrors
Berikut contoh panggilan GET /posts
dan memverifikasi bahwa vieew yang menerima $posts
variabel.
1 |
# app/tests/controllers/PostsControllerTest.php
|
2 |
|
3 |
public function testIndex() |
4 |
{
|
5 |
$this->call('GET', 'posts'); |
6 |
|
7 |
$this->assertViewHas('posts'); |
8 |
}
|
Tip: Ketika formatting, saya lebih suka untuk memberikan lne break antara pernyataan tes dan kode yang mempersiapkan stage.
assertViewHas
mempunyaikemudahan yang memeriksa obyek respon - yang dikembalikan dari $this->call()
- dan memverifikasi bahwa data yang terkait dengan tampilan yang mengandung sebuah variabel posts
.
Ketika memeriksa objek respon, Anda memiliki dua pilihan inti.
-
$response->getOriginalContent()
: mengambil konten asli, atau mengembalikanView
. Opsional, Anda dapat mengakses propertioriginal
secara langsung, alih-alih memanggil metodegetOriginalContent
. -
$response->getContent()
: me-render output diberikan. JikaView
instance return dari route, makagetContent()
akan sama dengan HTML output. Ini dapat membantu untuk verifikasi DOM, seperti "tampilan harus mengandung string ini."
Mari kita asumsikan bahwa route posts
terdiri dari:
1 |
<?php
|
2 |
|
3 |
# app/routes.php
|
4 |
|
5 |
Route::get('posts', function() |
6 |
{
|
7 |
return View::make('posts.index'); |
8 |
});
|
Kita harus menjalankan phpunit
, itu akan berkuak dengan pesan langkah berikutnya yang berguna:
1 |
1) PostsControllerTest::testIndex
|
2 |
Failed asserting that an array has the key 'posts'.
|
Untuk membuatnya hijau, kami hanya mengambil posting dan menyebarkannya ke tampilan.
1 |
|
2 |
# app/routes.php
|
3 |
|
4 |
Route::get('posts', function() |
5 |
{
|
6 |
$posts = Post::all(); |
7 |
|
8 |
return View::make('posts.index', ['posts', $posts]); |
9 |
});
|
Satu hal yang perlu diingat adalah bahwa, seperti kode saat ini berdiri, itu hanya memastikan bahwa variabel, $posts
, passing ke view. Itu tidak memeriksa value. AssertViewHas
opsional menerima argumen kedua untuk memverifikasi nilai variabel, serta keberadaannya.
1 |
|
2 |
# app/tests/controllers/PostsControllerTest.php
|
3 |
|
4 |
public function testIndex() |
5 |
{
|
6 |
$this->call('GET', 'posts'); |
7 |
|
8 |
$this->assertViewHas('posts', 'foo'); |
9 |
}
|
Dengan kode ini dimodifikasi, unles view memiliki sebuah variabel, $posts
, yang sama dengan foo
, tes akan gagal. Dalam situasi ini, walaupun, ini kemungkinan bahwa kita akan agak tidak menetapkan nilai, tetapi justru menyatakan bahwa nilai menjadi instance Laravel's Illuminate\Database\Eloquent\Collection
kelas. Bagaimana kita bisa mencapai itu? PHPUnit menyediakan pernyataan assertInstanceOf
berguna untuk mengisi kebutuhan ini!
1 |
|
2 |
# app/tests/controllers/PostsControllerTest.php
|
3 |
|
4 |
public function testIndex() |
5 |
{
|
6 |
$response = $this->call('GET', 'posts'); |
7 |
|
8 |
$this->assertViewHas('posts'); |
9 |
|
10 |
// getData() returns all vars attached to the response.
|
11 |
$posts = $response->original->getData()['posts']; |
12 |
|
13 |
$this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $posts); |
14 |
}
|
Dengan modifikasi ini, kami telah menyatakan bahwa controller harus pass $posts
- instance Illuminate\Database\Eloquent\Collection
- ke view. Sangat baik.
Mocking Database
Ada satu masalah dengan pengujian kami sejauh ini. Apakah Anda menangkap itu?
Untuk setiap tes, SQL query yang dijalankan pada database. Meskipun ini berguna untuk beberapa jenis pengujian (acceptance, integraion), untuk controller dasar testing, itu hanya akan berfungsi untuk mengurangi kinerja.
Saya telah dibor ini ke tengkorak beberapa kali pada saat ini. Kami tidak tertarik dengan testing Eloquent kemampuan untuk mengambil record dari database. Ini memiliki tesnya sendiri. Taylor tahu itu bekerja! Mari kita tidak menyia-nyiakan waktu dan kekuatan pemrosesan mengulang tes yang sama.
Sebaliknya, lebih baik untuk mock database, dan hanya memverifikasi bahwa metode yang sesuai dipanggil dengan argumen-argumen yang benar. Atau, dengan kata lain, kami ingin memastikan bahwa Post::all()
tidak dipanggil dan hits database. Kita tahu bahwa itu bekerja, sehingga tidak memerlukan pengujian.
Bagian ini akan sangat bergantung pada perpustakaan Mocked. Harap Tinjau bahwa bab dari buku saya, jika Anda tidak belum terbiasa dengan hal itu.
Diperlukan Refactoring
Sayangnya, sejauh ini, kami telah terstruktur kode dalam cara yang membuatnya hampir mustahil untuk menguji.
1 |
# app/routes.php
|
2 |
|
3 |
Route::get('posts', function() |
4 |
{
|
5 |
// Ouch. We can't test this!!
|
6 |
$posts = Post::all(); |
7 |
|
8 |
return View::make('posts.index') |
9 |
->with('posts', $posts); |
10 |
});
|
Inilah mengapa dianggap buruk praktek ke nested Eloquent panggilan ke controller Anda. Jangan bingung Laravel Facade, yang dapat diuji dan dapat ditukarkan dengan mock (Queue::shouldReceive()
), dengan model Eloquent anda. Solusinya adalah untuk inject lapisan database ke controller melalui konstruktor. Hal ini memerlukan beberapa refactoring.
Warning: Menyimpan logika dalam rute callback berguna untuk proyek-proyek kecil dan api, tapi mereka membuat pengujian sangat sulit. Untuk aplikasi dari berbagai ukuran yang cukup besar, menggunakan controller.
Mari kita mendaftar sumber daya baru dengan mengganti rute posts
dengan:
1 |
# app/routes.php
|
2 |
|
3 |
Route::resource('posts', 'PostsController'); |
.. .dan membuat controller resourceful dengan Artisan.
1 |
$ php artisan controller:make PostsController
|
2 |
Controller created successfully! |
Sekarang, daripada referensi Post
model langsung, kami akan inject itu ke controller konstruktor. Berikut adalah contoh yang menghilangkan semua metode restful kecuali satu bahwa kami tertarik saat ini dalam testing.
1 |
<?php
|
2 |
|
3 |
# app/controllers/PostsController.php
|
4 |
|
5 |
class PostsController extends BaseController { |
6 |
|
7 |
protected $post; |
8 |
|
9 |
public function __construct(Post $post) |
10 |
{
|
11 |
$this->post = $post; |
12 |
}
|
13 |
|
14 |
public function index() |
15 |
{
|
16 |
$posts = $this->post->all(); |
17 |
|
18 |
return View::make('posts.index') |
19 |
->with('posts', $posts); |
20 |
}
|
21 |
|
22 |
}
|
Harap dicatat bahwa itu adalah ide yang baik untuk typehint antarmuka, daripada referensi model Eloquent, itu sendiri. Namun, satu hal pada suatu waktu! Mari kita bekerja untuk itu.
Ini adalah cara yang secara signifikan lebih baik untuk struktur kode. Karena model sekarang sudah di inject, kita memiliki kemampuan untuk swap keluar dengan versi mocked untuk pengujian. Berikut adalah contoh dari melakukan hal itu:
1 |
<?php
|
2 |
|
3 |
# app/tests/controllers/PostsControllerTest.php
|
4 |
|
5 |
class PostsControllerTest extends TestCase { |
6 |
|
7 |
public function __construct() |
8 |
{
|
9 |
// We have no interest in testing Eloquent
|
10 |
$this->mock = Mockery::mock('Eloquent', 'Post'); |
11 |
}
|
12 |
|
13 |
public function tearDown() |
14 |
{
|
15 |
Mockery::close(); |
16 |
}
|
17 |
|
18 |
public function testIndex() |
19 |
{
|
20 |
$this->mock |
21 |
->shouldReceive('all') |
22 |
->once() |
23 |
->andReturn('foo'); |
24 |
|
25 |
$this->app->instance('Post', $this->mock); |
26 |
|
27 |
$this->call('GET', 'posts'); |
28 |
|
29 |
$this->assertViewHas('posts'); |
30 |
}
|
31 |
|
32 |
}
|
Benfit kunci untuk restrukturisasi ini adalah bahwa, sekarang, database tidak akan sia-sia terkena. Sebaliknya, menggunakan Mockery, kami hanya memverifikasi bahwa all
metode dipicu pada model.
1 |
$this->mock |
2 |
->shouldReceive('all') |
3 |
->once(); |
Sayangnya, jika Anda memilih untuk mengorbankan pengkodean untuk antarmuka, dan sebaliknya menyuntikkan Post
model ke controller, sedikit tipu daya harus digunakan untuk mendapatkan Eloquent penggunaan Statika, yang dapat bentrokan dengan Mockery. Inilah sebabnya mengapa kita hijack Post
dan Eloquent
kelas dalam ujian konstruktor, sebelum versi resmi telah dibuka. Dengan cara ini, kita memiliki yang bersih untuk menyatakan harapan. The downside, tentu saja, adalah bahwa kita tidak bisa default untuk setiap metode yang ada, melalui penggunaan metode Mockery, seperti makePartial()
.
IoC Container
Laravel's IoC Container drastis memudahkan proses menyuntikkan dependensi ke dalam kelas Anda. Setiap kali sebuah controller yang diminta, itu diselesaikan dari IoC Container. Dengan demikian, ketika kita harus menyatakan bahwa versi mock Post
harus digunakan untuk pengujian, kita hanya perlu memberikan Laravel dengan contoh Post
yang harus digunakan.
1 |
$this->app->instance('Post', $this->mock); |
Pikirkan kode ini mengatakan, "Hei Laravel, ketika Anda membutuhkan instance dari
Post
, saya ingin Anda untuk menggunakan versi mocked saya." Karena app extendContainer
, kita memiliki akses ke semua IoC metode langsung dari itu.
Berdasarkan Instansiasi controller, Laravel memanfaatkan kekuatan dari PHP refleksi untuk membaca typehint dan menyuntikkan ketergantungan untuk Anda. Benar; Anda tidak perlu menulis mengikat tunggal untuk memungkinkan ini; Hal ini otomatis!
Redirections
expectation umum lain bahwa Anda akan menemukan diri menulis adalah salah satu yang menjamin bahwa pengguna diarahkan ke lokasi yang tepat, mungkin setelah menambahkan posting baru ke database. Bagaimana mungkin kami mencapai ini?
1 |
# app/tests/controllers/PostsControllerTest.php
|
2 |
|
3 |
public function testStore() |
4 |
{
|
5 |
$this->mock |
6 |
->shouldReceive('create') |
7 |
->once(); |
8 |
|
9 |
$this->app->instance('Post', $this->mock); |
10 |
|
11 |
$this->call('POST', 'posts'); |
12 |
|
13 |
$this->assertRedirectedToRoute('posts.index'); |
14 |
|
15 |
}
|
Dengan asumsi bahwa kita mengikuti restfull, untuk menambahkan posting baru, kami akan POST
ke koleksi, atau posts
(jangan bingung metode permintaan POST
dengan nama resource, yang kebetulan memiliki nama yang sama).
1 |
$this->call('POST', 'posts'); |
Kemudian, kita hanya perlu untuk memanfaatkan salah satu Laravel's helper , assertRedirectedToRoute
.
Tip: Ketika resouce yang terdaftar dengan Laravel (
Route::resource()
), framework akan secara otomatis mendata nama route yang diperlukan. Menjalankanphp artisan routes
jika Anda lupa apa yang nama-nama ini.
Anda mungkin lebih suka juga memastikan bahwa $_POST
superglobal dilewatkan ke metode create
. Meskipun kita tidak secara langsung submit form, kita masih bisa memungkinkan untuk ini, melalui metode Input:: replace()
, yang memungkinkan kita untuk "stub" array. Berikut adalah tes diubah, yang menggunakan metode Mockery with
untuk memverifikasi argumen yang dilewatkan ke metode direferensikan oleh shouldReceive
.
1 |
# app/tests/controllers/PostsControllerTest.php
|
2 |
|
3 |
public function testStore() |
4 |
{
|
5 |
Input::replace($input = ['title' => 'My Title']);</p> |
6 |
|
7 |
$this->mock |
8 |
->shouldReceive('create') |
9 |
->once() |
10 |
->with($input); |
11 |
|
12 |
$this->app->instance('Post', $this->mock); |
13 |
|
14 |
$this->call('POST', 'posts'); |
15 |
|
16 |
$this->assertRedirectedToRoute('posts.index'); |
17 |
}
|
Path
Satu hal yang kita belum dipertimbangkan dalam tes ini adalah validasi. Harus ada dua jalur yang terpisah melalui metode store
, tergantung pada apakah validasi berhasil:
- Mengarahkan kembali ke bentuk "Create Post", dan menampilkan kesalahan validasi form.
- Mengarahkan ulang ke koleksi, atau rute bernama,
posts.index
.
Sebagai best practice, setiap tes harus mewakili tapi satu path melalui kode Anda.
path pertama ini akan untuk validasi gagal.
1 |
# app/tests/controllers/PostsControllerTest.php
|
2 |
|
3 |
public function testStoreFails() |
4 |
{
|
5 |
// Set stage for a failed validation
|
6 |
Input::replace(['title' => '']); |
7 |
|
8 |
$this->app->instance('Post', $this->mock); |
9 |
|
10 |
$this->call('POST', 'posts'); |
11 |
|
12 |
// Failed validation should reload the create form
|
13 |
$this->assertRedirectedToRoute('posts.create'); |
14 |
|
15 |
// The errors should be sent to the view
|
16 |
$this->assertSessionHasErrors(['title']); |
17 |
}
|
Snipet kode di atas secara eksplisit menyatakan kesalahan yang harus ada. Atau, Anda dapat menghilangkan argumen ke assertSessionHasErrors
, dalam hal ini hanya akan memverifikasi bahwa pesan tas telah flashed (dalam terjemahan, pengalihan Anda termasuk withErrors($errors)
).
Sekarang untuk test yang menangani berhasil tervalidasi.
1 |
# app/tests/controllers/PostsControllerTest.php
|
2 |
|
3 |
public function testStoreSuccess() |
4 |
{
|
5 |
// Set stage for successful validation
|
6 |
Input::replace(['title' => 'Foo Title']);</p> |
7 |
|
8 |
$this->mock |
9 |
->shouldReceive('create') |
10 |
->once(); |
11 |
|
12 |
$this->app->instance('Post', $this->mock); |
13 |
|
14 |
$this->call('POST', 'posts'); |
15 |
|
16 |
// Should redirect to collection, with a success flash message
|
17 |
$this->assertRedirectedToRoute('posts.index', ['flash']); |
18 |
}
|
Kode produksi untuk kedua ujian mungkin terlihat seperti:
1 |
# app/controllers/PostsController.php
|
2 |
|
3 |
public function store() |
4 |
{
|
5 |
$input = Input::all(); |
6 |
|
7 |
// We'll run validation in the controller for convenience
|
8 |
// You should export this to the model, or a service
|
9 |
$v = Validator::make($input, ['title' => 'required']); |
10 |
|
11 |
if ($v->fails()) |
12 |
{
|
13 |
return Redirect::route('posts.create') |
14 |
->withInput() |
15 |
->withErrors($v->messages()); |
16 |
}
|
17 |
|
18 |
$this->post->create($input); |
19 |
|
20 |
return Redirect::route('posts.index') |
21 |
->with('flash', 'Your post has been created!'); |
22 |
}
|
Perhatikan bagaimana Validator
yang bersarang langsung di controller? Umumnya, saya akan merekomendasikan bahwa Anda abstrak ini pergi ke service. Dengan cara itu, Anda dapat menguji Anda validasi dalam isolasi dari controller atau rute. Namun, mari kita meninggalkan hal-hal seperti mereka demi kesederhanaan. Satu hal yang perlu diingat adalah bahwa kita tidak mocking Validator
, meskipun Anda pasti bisa melakukannya. Karena kelas ini sebuah facade, itu dapat dengan mudah bertukar keluar dengan versi mocking, melalui metode shouldReceive
Facade, tanpa kita perlu khawatir tentang menyuntikkan instance melalui konstruktor. Menang!
1 |
# app/controllers/PostsController.php
|
2 |
|
3 |
Validator::shouldReceive('make') |
4 |
->once() |
5 |
->andReturn(Mockery::mock(['fails' => 'true'])); |
Dari waktu ke waktu, Anda akan menemukan bahwa metode yang perlu akan dipermainkan harus kembali objek, itu sendiri. Untungnya, dengan Mokcery, ini adalah sepotong kue: kita hanya perlu anonim mock, dan parsing array, yang sinyal metode nama dan respon nilai, masing-masing. Seperti:
1 |
Mockery::mock(['fails' => 'true']) |
akan mempersiapkan objek, yang mengandung fails()
metode yang mengembalikan true
.
Repositori
Untuk memungkinkan untuk hasil yang optimal fleksibilitas, daripada menciptakan sebuah link langsung antara controller dan ORM, seperti fasih, itu lebih baik untuk kode untuk sebuah interface. Keuntungan besar dari pendekatan ini adalah bahwa, jika Anda mungkin perlu swap keluar Eloquent untuk, katakanlah, Mongo atau Redis, melakukannya benar-benar membutuhkan modifikasi dari satu baris. Bahkan lebih baik, controller tidak pernah perlu disentuh.
Repositori mewakili layer akses data aplikasi Anda.
Apa yang mungkin sebuah antarmuka untuk mengelola lapisan database Post
terlihat seperti? Ini harus Anda mulai.
1 |
<?php
|
2 |
|
3 |
# app/repositories/PostRepositoryInterface.php
|
4 |
|
5 |
interface PostRepositoryInterface { |
6 |
|
7 |
public function all(); |
8 |
|
9 |
public function find($id); |
10 |
|
11 |
public function create($input); |
12 |
|
13 |
}
|
Ini tentu saja dapat diperpanjang, tapi kami telah menambahkan metode minimal untuk demo: all
, find
, dan create
. Perhatikan bahwa interface repositori yang disimpan dalam app/repositories
. Karena folder ini tidak otomatis diambil secara default, kita perlu untuk memperbarui file composer.json
untuk aplikasi untuk referensi.
1 |
// composer.json
|
2 |
|
3 |
"autoload": { |
4 |
"classmap": [ |
5 |
// ....
|
6 |
"app/repositories" |
7 |
]
|
8 |
}
|
Ketika kelas baru ditambahkan ke direktori ini, jangan lupa untuk
composer dump-autoload - o
.-o
, (mengoptimalkan) flag opsional, tetapi harus selalu digunakan, sebagai penerapan terbaik.
Jika Anda mencoba untuk inject interface ini ke controller, Laravel akan snap pada Anda. Silahkan mencobanya dan melihat. Berikut adalah PostController
dimodifikasi, yang telah diperbarui untuk inject interface, daripada model Post
Eloquent.
1 |
<?php
|
2 |
|
3 |
# app/controllers/PostsController.php
|
4 |
|
5 |
use Repositories\PostRepositoryInterface as Post; |
6 |
|
7 |
class PostsController extends BaseController { |
8 |
|
9 |
protected $post; |
10 |
|
11 |
public function __construct(Post $post) |
12 |
{
|
13 |
$this->post = $post; |
14 |
}
|
15 |
|
16 |
public function index() |
17 |
{
|
18 |
$posts = $this->post->all(); |
19 |
|
20 |
return View::make('posts.index', ['posts' => $posts]); |
21 |
}
|
22 |
|
23 |
}
|
Jika Anda menjalankan server dan melihat output, Anda akan dipenuhi dengan kesalahan halaman Whoops (tapi indah) ini, menyatakan bahwa "PostRepositoryInterface tidak instantiable."



Jika Anda berpikir tentang hal itu, tentu saja framework adalah berkotek! Laravel cerdas, tetapi tidak pembaca pikiran. Perlu diberitahu implementasi interface yang harus digunakan di dalam controller.
Untuk sekarang, mari kita binding ke app/routes.php
. Kemudian, kita akan sebaliknya membuat menggunakan service provider untuk menyimpan logika semacam ini.
1 |
# app/routes.php
|
2 |
|
3 |
App::bind( |
4 |
'Repositories\PostRepositoryInterface', |
5 |
'Repositories\EloquentPostRepository'
|
6 |
);
|
Verbalisasi panggilan fungsi ini sebagai, "Laravel, bayi, ketika Anda membutuhkan sebuah instance dari PostRepositoryInterface
, saya ingin Anda untuk menggunakan EloquentPostRepository
."
app/repositori/EloquentPostRepository
hanya akan bungkus sekitar Eloquent yang mengimplementasikan PostRepositoryInterface
. Dengan cara ini, kami sedang tidak membatasi API (dan setiap implementasi lain) untuk interpretasi Ekiquent 's; kita dapat nama metode namun kami berharap.
1 |
<?php namespace Repositories; |
2 |
|
3 |
# app/repositories/EloquentPostRepository.php
|
4 |
|
5 |
use Repositories\PostRepositoryInterface; |
6 |
use Post; |
7 |
|
8 |
class EloquentPostRepository implements PostRepositoryInterface { |
9 |
|
10 |
public function all() |
11 |
{
|
12 |
return Post::all(); |
13 |
}
|
14 |
|
15 |
public function find($id) |
16 |
{
|
17 |
return Post::find($id); |
18 |
}
|
19 |
|
20 |
public function create($input) |
21 |
{
|
22 |
return Post::create($input); |
23 |
}
|
24 |
|
25 |
}
|
Beberapa mungkin berpendapat bahwa
Post
model harus inject ke dalam implementasi ini untuk tujuan testability. Jika Anda setuju, hanya inject melalui konstruktor, per biasa.
Itu saja yang diperlukan! Refresh browser, dan hal-hal yang harus kembali ke normal. Hanya sekarang, aplikasi Anda jauh lebih baik terstruktur, dan controller tidak lagi terhubung dengan Eloquent.
Mari kita bayangkan bahwa, beberapa bulan dari sekarang, bos Anda memberitahu Anda bahwa Anda perlu untuk menukar Eloquent dengan Redis. Nah, karena Anda telah terstruktur aplikasi Anda di masa depan-bukti dengan cara ini, Anda hanya perlu membuat implementasi app/repositories/RedisPostRepository
baru:
1 |
<?php namespace Repositories; |
2 |
|
3 |
# app/repositories/RedisPostRepository.php
|
4 |
|
5 |
use Repositories\PostRepositoryInterface; |
6 |
|
7 |
class RedisPostRepository implements PostRepositoryInterface { |
8 |
|
9 |
public function all() |
10 |
{
|
11 |
// return all with Redis
|
12 |
}
|
13 |
|
14 |
public function find($id) |
15 |
{
|
16 |
// return find one with Redis
|
17 |
}
|
18 |
|
19 |
public function create($input) |
20 |
{
|
21 |
// return create with Redis
|
22 |
}
|
23 |
|
24 |
}
|
Dan update binding:
1 |
# app/routes.php
|
2 |
|
3 |
App::bind( |
4 |
'Repositories\PostRepositoryInterface', |
5 |
'Repositories\RedisPostRepository'
|
6 |
);
|
Seketika, Anda sekarang sedang memanfaatkan Redis dalam controller. Perhatikan bagaimana app/controllers/PostsController.php
tidak pernah menyentuh? Itulah keindahan itu!
Struktur
Dalam pelajaran ini, organisasi kami sejauh ini agak kurang. IoC binding dalam routes.php
file? Semua repositori dikelompokkan bersama dalam satu direktori? Tentu, itu mungkin bekerja di awal, tapi, sangat cepat, itu akan menjadi jelas bahwa ini tidak skala.
Di bagian akhir artikel ini, kita akan PSR-ify kode kita, dan memanfaatkan service provider untuk mendaftar setiap binding yang berlaku.
PSR-0 mendefinisikan persyaratan wajib yang harus ditaati untuk autoloader interoperabilitas.
Sebuah loader PSR-0 dapat didaftarkan dengan composer, melalui objek psr-0
.
1 |
// composer.json
|
2 |
|
3 |
"autoload": { |
4 |
"psr-0": { |
5 |
"Way": "app/lib/" |
6 |
}
|
7 |
}
|
Sintaks dapat membingungkan pada awalnya. Jelas itu bagi saya. Cara mudah untuk menguraikan "Way": "app/lib/
" adalah berpikir untuk diri sendiri, "folder dasar Way
namespace terletak di app/lib." Tentu saja, mengganti nama terakhir saya dengan nama proyek Anda. Struktur direktori untuk pertandingan ini akan menjadi:
- app/
- lib/
- Way/
Selanjutnya, daripada pengelompokan semua repositori ke dalam direktori repositories
, pendekatan yang lebih elegan mungkin untuk mengkategorikan mereka ke dalam beberapa direktori, seperti:
- app/
- lib/
- Way/
- Storage/
- Post/
- PostRepositoryInterface.php
- EloquentPostRepository.php
Sangat penting bahwa kami mematuhi konvensi penamaan dan folder ini, jika kita ingin autoloading untuk bekerja seperti yang diharapkan. Satu-satunya hal tersisa untuk dilakukan adalah memperbarui namespaces untuk PostRepositoryInterface
dan EloquentPostRepository
.
1 |
<?php namespace Way\Storage\Post; |
2 |
|
3 |
# app/lib/Way/Storage/Post/PostRepositoryInterface.php
|
4 |
|
5 |
interface PostRepositoryInterface { |
6 |
|
7 |
public function all(); |
8 |
|
9 |
public function find($id); |
10 |
|
11 |
public function create($input); |
12 |
|
13 |
}
|
Dan untuk implementasi:
1 |
<?php namespace Way\Storage\Post; |
2 |
|
3 |
# app/lib/Way/Storage/Post/EloquentPostRepository.php
|
4 |
|
5 |
use Post; |
6 |
|
7 |
class EloquentPostRepository implements PostRepositoryInterface { |
8 |
|
9 |
public function all() |
10 |
{
|
11 |
return Post::all(); |
12 |
}
|
13 |
|
14 |
public function find($id) |
15 |
{
|
16 |
return Post::find($id); |
17 |
}
|
18 |
|
19 |
public function create($input) |
20 |
{
|
21 |
return Post::create($input); |
22 |
}
|
23 |
|
24 |
}
|
Sana kami pergi; itu jauh lebih bersih. Tapi bagaimana dengan binding? Rute file mungkin merupakan tempat yang nyaman untuk bereksperimen, tetapi itu tidak masuk akal untuk menyimpan mereka tidak secara permanen. Sebaliknya, kita akan menggunakan service provider.
Service provider yang tidak lebih dari bootstrap kelas yang dapat digunakan untuk melakukan apa pun yang Anda inginkan: mendaftar mengikat, menghubungkan ke dalam sebuah event, impor file route, dll.
register()
Service Provider akan dipicu secara otomatis oleh Laravel.
1 |
<?php namespace Way\Storage; |
2 |
|
3 |
# app/lib/Way/Storage/StorageServiceProvider.php
|
4 |
|
5 |
use Illuminate\Support\ServiceProvider; |
6 |
|
7 |
class StorageServiceProvider extends ServiceProvider { |
8 |
|
9 |
// Triggered automatically by Laravel
|
10 |
public function register() |
11 |
{
|
12 |
$this->app->bind( |
13 |
'Way\Storage\Post\PostRepositoryInterface', |
14 |
'Way\Storage\Post\EloquentPostRepository'
|
15 |
);
|
16 |
}
|
17 |
|
18 |
}
|
Untuk membuat file ini dikenal untuk Laravel, Anda hanya perlu untuk memasukkan dalam app/config/app.php
, dalam array providers
.
1 |
# app/config/app.php
|
2 |
|
3 |
'providers' => array( |
4 |
'Illuminate\Foundation\Providers\ArtisanServiceProvider', |
5 |
'Illuminate\Auth\AuthServiceProvider', |
6 |
// ...
|
7 |
'Way\Storage\StorageServiceProvider'
|
8 |
)
|
Baik; Sekarang kita memiliki sebuah file yang didedikasikan untuk mendaftar binding baru.
Memperbarui tes
Dengan struktur baru kami di tempat, daripada mocking model Eloquent, itu sendiri, kami dapat sebaliknya mocking PostRepositoryInterface
. Berikut adalah contoh dari satu tes tersebut:
1 |
# app/tests/controllers/PostsControllerTest.php
|
2 |
|
3 |
public function testIndex() |
4 |
{
|
5 |
$mock = Mockery::mock('Way\Storage\Post\PostRepositoryInterface'); |
6 |
$mock->shouldReceive('all')->once(); |
7 |
|
8 |
$this->app->instance('Way\Storage\Post\PostRepositoryInterface', $mock); |
9 |
|
10 |
$this->call('GET', 'posts'); |
11 |
|
12 |
$this->assertViewHas('posts'); |
13 |
}
|
Namun, kita dapat memperbaiki ini. Masuk akal bahwa setiap metode dalam PostsControllerTest
akan memerlukan versi mock dari repositori. Dengan demikian, lebih baik untuk mengekstrak beberapa karya persiapan ini menjadi metode sendiri, seperti:
1 |
# app/tests/controllers/PostsControllerTest.php
|
2 |
|
3 |
public function setUp() |
4 |
{
|
5 |
parent::setUp(); |
6 |
|
7 |
$this->mock('Way\Storage\Post\PostRepositoryInterface'); |
8 |
}
|
9 |
|
10 |
public function mock($class) |
11 |
{
|
12 |
$mock = Mockery::mock($class); |
13 |
|
14 |
$this->app->instance($class, $mock); |
15 |
|
16 |
return $mock; |
17 |
}
|
18 |
|
19 |
public function testIndex() |
20 |
{
|
21 |
$this->mock->shouldReceive('all')->once(); |
22 |
|
23 |
$this->call('GET', 'posts'); |
24 |
|
25 |
$this->assertViewHas('posts'); |
26 |
}
|
Tidak buruk, ay?
Sekarang, jika Anda ingin menjadi super-fly, dan bersedia untuk menambahkan sentuhan tes logika kode produksi, Anda bahkan bisa melakukan mocking dalam model Eloquent! Hal ini akan memungkinkan untuk:
1 |
Post::shouldReceive('all')->once(); |
Di belakang layar, ini akan mengejek PostRepositoryInterface
, dan memperbarui binding IoC. Anda tidak mendapatkan jauh lebih mudah dibaca dari itu!
Memungkinkan untuk sintaks ini hanya membutuhkan Anda untuk memperbarui Post
model, atau, lebih baik, BaseModel
bahwa semua model Elqouent extend itu. Berikut adalah contoh bekas:
1 |
<?php
|
2 |
|
3 |
# app/models/Post.php
|
4 |
|
5 |
class Post extends Eloquent { |
6 |
|
7 |
public static function shouldReceive() |
8 |
{
|
9 |
$class = get_called_class(); |
10 |
$repo = "Way\\Storage\\{$class}\\{$class}RepositoryInterface"; |
11 |
$mock = Mockery::mock($repo); |
12 |
|
13 |
App::instance($repo, $mock); |
14 |
|
15 |
return call_user_func_array([$mock, 'shouldReceive'], func_get_args()); |
16 |
}
|
17 |
|
18 |
}
|
Jika Anda dapat mengatur "Harus saya menjadi embedding tes logika ke dalam kode produksi" dalam pertempuran, Anda akan menemukan bahwa hal ini memungkinkan untuk tes secara signifikan lebih mudah dibaca.
1 |
<?php
|
2 |
|
3 |
# app/tests/controllers/PostsControllerTest.php
|
4 |
|
5 |
class PostsControllerTest extends TestCase { |
6 |
|
7 |
public function tearDown() |
8 |
{
|
9 |
Mockery::close(); |
10 |
}
|
11 |
|
12 |
public function testIndex() |
13 |
{
|
14 |
Post::shouldReceive('all')->once(); |
15 |
|
16 |
$this->call('GET', 'posts'); |
17 |
|
18 |
$this->assertViewHas('posts'); |
19 |
}
|
20 |
|
21 |
public function testStoreFails() |
22 |
{
|
23 |
Input::replace($input = ['title' => '']); |
24 |
|
25 |
$this->call('POST', 'posts'); |
26 |
|
27 |
$this->assertRedirectedToRoute('posts.create'); |
28 |
$this->assertSessionHasErrors(); |
29 |
}
|
30 |
|
31 |
public function testStoreSuccess() |
32 |
{
|
33 |
Input::replace($input = ['title' => 'Foo Title']); |
34 |
|
35 |
Post::shouldReceive('create')->once(); |
36 |
|
37 |
$this->call('POST', 'posts'); |
38 |
|
39 |
$this->assertRedirectedToRoute('posts.index', ['flash']); |
40 |
}
|
41 |
|
42 |
}
|
Rasanya enak, bukan? Mudah-mudahan, artikel ini belum terlalu berlebihan. Kuncinya adalah untuk belajar bagaimana untuk mengatur repositori Anda sedemikian rupa untuk membuatnya semudah mungkin untuk mocking dan inject ke controller Anda. Sebagai hasil dari upaya itu, tes Anda akan kilat cepat!
Artikel ini adalah kutipan dari buku mendatang, Laravel Testing Decoded. Menantikan untuk rilis pada Mei 2013!