Unit testing di Laravel

Di antara diskusi di masyarakat, saya sering mendengar pendapat bahwa pengujian unit di Laravel salah, rumit, dan tes itu sendiri panjang dan tidak memberikan manfaat apa pun. Karena itu, beberapa menulis tes ini, membatasi diri mereka hanya untuk tes fitur, dan tes unit manfaat menuju 0.
Saya juga pernah berpikir begitu, tetapi begitu saya memikirkannya dan bertanya pada diri sendiri - mungkin saya tidak tahu cara memasaknya?


Untuk beberapa waktu saya mengerti dan di pintu keluar saya memiliki pemahaman baru tentang tes unit, dan tes menjadi jelas, ramah, cepat dan mulai membantu saya.
Saya ingin berbagi pemahaman saya dengan komunitas, dan bahkan lebih memahami topik ini, membuat tes saya lebih baik.


Sedikit filosofi dan batasan


Laravel adalah semacam kerangka kerja di beberapa tempat. Terutama dari segi fasad dan Eloquent. Saya tidak akan menyentuh pada diskusi atau kecaman dari poin-poin ini, tetapi saya akan menunjukkan bagaimana saya menggabungkannya dengan unit test.
Saya menulis tes setelah (atau pada saat yang sama) menulis kode utama. Mungkin pendekatan saya tidak akan kompatibel dengan pendekatan TDD atau memerlukan penyesuaian parsial.


Pertanyaan paling penting yang saya tanyakan pada diri sendiri sebelum menulis tes adalah "apa sebenarnya yang ingin saya uji?". Ini adalah masalah penting. Gagasan inilah yang memungkinkan saya untuk mempertimbangkan kembali pandangan saya tentang penulisan tes unit dan kode proyek itu sendiri.


Tes harus stabil dan minimal tergantung pada lingkungan. Jika, ketika Anda membuat mutasi, tes Anda gagal, kemungkinan besar itu bagus. Sebaliknya, jika mereka tidak jatuh, mereka mungkin tidak terlalu baik.


Di luar kotak, Laravel mendukung 3 jenis tes:


  • Browser
  • Fitur
  • Unit

Saya terutama akan berbicara tentang tes Unit.


Saya tidak menguji semua kode melalui unit test (mungkin ini tidak benar). Saya tidak menguji beberapa kode sama sekali (lebih lanjut tentang ini di bawah).


Jika moque digunakan dalam tes, jangan lupa untuk melakukan Mockery :: close () pada tearDown.


Beberapa contoh tes "diambil dari Internet."


Bagaimana saya menguji


Di bawah ini saya akan mengelompokkan contoh uji menurut kelompok kelas dan mencoba memberikan contoh uji untuk setiap kelompok kelas. Bagi sebagian besar kelompok kelas, saya tidak akan memberikan contoh kode itu sendiri.


Middleware


Untuk pengujian unit middleware, saya membuat objek dari kelas Permintaan, sebuah objek dari Middleware yang diinginkan, kemudian saya memanggil metode pegangan dan menjalankan pernyataan yang diperlukan. Middleware sesuai dengan tindakan yang dilakukan dapat dibagi menjadi 3 kelompok:


  • mengubah objek permintaan (mengubah permintaan tubuh, atau sesi)
  • pengarahan ulang (mengubah status respons)
  • tidak melakukan apa-apa dengan objek permintaan
    Mari kita coba memberikan contoh tes untuk setiap kelompok:

Misalkan kita memiliki Middleware berikut, yang tugasnya memodifikasi bidang judul:


class TitlecaseMiddleware { public function handle($request, Closure $next) { if ($request->title) { $request->merge([ 'title' => title_case($request->title) ]); } return $next($request); } } 

Tes untuk Middleware serupa mungkin terlihat seperti ini:


 public function testChangeTitleToTitlecase() { $request = new Request; $request->merge([ 'title' => 'Title is in mixed CASE' ]); $middleware = new TitlecaseMiddleware; $middleware->handle($request, function ($req) { $this->assertEquals('Title Is In Mixed Case', $req->title); }); } 

Tes untuk kelompok 2 dan 3 terdiri dari rencana semacam itu, masing-masing:


 $response = $middleware->handle($request, function () {}); $this->assertEquals($response->getStatusCode(), 302); //   $this->assertEquals($response, null); //      request 

Minta kelas


Tugas utama grup kelas ini adalah otorisasi dan validasi permintaan.


Saya tidak menguji kelas-kelas ini melalui tes unit (saya akui bahwa ini mungkin tidak benar), hanya melalui tes fitur. Menurut pendapat saya, tes unit berlebihan untuk kelas-kelas ini, tetapi saya menemukan beberapa contoh menarik tentang bagaimana hal ini dapat dilakukan. Mungkin mereka akan membantu Anda jika Anda memutuskan untuk menguji kelas unit permintaan Anda dengan tes:



Pengendali


Saya juga tidak menguji pengontrol melalui unit test. Tetapi ketika mengujinya, saya menggunakan satu fitur yang ingin saya bicarakan.


Pengendali, menurut saya, harus ringan. Tugas mereka adalah mendapatkan permintaan yang benar, memanggil layanan dan repositori yang diperlukan (karena kedua istilah ini "asing" bagi Laravel, saya akan menjelaskan terminologi saya di bawah), mengembalikan jawabannya. Terkadang memicu suatu peristiwa, Pekerjaan, dll.
Dengan demikian, ketika menguji melalui pengujian fitur, kita tidak hanya perlu memanggil controller dengan parameter yang diperlukan dan memeriksa jawabannya, tetapi juga mengunci layanan yang diperlukan dan memverifikasi bahwa mereka benar-benar dipanggil (atau tidak dipanggil). Terkadang - membuat catatan dalam database.


Contoh uji pengontrol dengan tiruan kelas layanan:


 public function testProductCategorySync() { $service = Mockery::mock(\App\Services\Product::class); app()->instance(\App\Services\Product::class, $service); $service->shouldReceive('sync')->once(); $response = $this->post('/api/v1/sync/eventsCallback', [ "eventType" => "PRODUCT_SYNC" ]); $response->assertStatus(200); } 

Contoh uji pengontrol dengan tiruan fasad (dalam kasus kami, peristiwa, tetapi dengan analogi dilakukan untuk fasad Laravel lainnya):


 public function testChangeCart() { Event::fake(); $user = factory(User::class)->create(); Passport::actingAs( $user ); $response = $this->post('/api/v1/cart/update', [ 'products' => [ [ // our changed data ] ], ]); $data = json_decode($response->getContent()); $response->assertStatus(200); $this->assertEquals($user->id, $data->data->userId); // and assert other data from response Event::assertDispatched(CartChanged::class); } 

Layanan dan Repositori


Jenis kelas ini di luar kotak. Saya mencoba untuk menjaga pengendali tetap tipis, jadi saya menempatkan semua pekerjaan ekstra ke dalam salah satu dari kelompok kelas ini.


Saya menentukan perbedaan di antara mereka sebagai berikut:


  • Jika saya perlu menerapkan beberapa logika bisnis, maka saya meletakkan ini di lapisan layanan yang sesuai (kelas).
  • Dalam semua kasus lain, saya meletakkan ini di grup kelas repositori. Sebagai aturan, fungsional dengan Eloquent pergi ke sana. Saya mengerti bahwa ini bukan definisi yang tepat untuk level repositori. Saya juga mendengar bahwa beberapa orang menanggung segala hal yang berkaitan dengan Eloquent dalam model. Pendekatan saya adalah semacam kompromi, menurut saya, meskipun "secara akademis" tidak sepenuhnya benar.

Untuk kelas-kelas Repositori, saya hampir tidak menulis tes.


Contoh uji kelas layanan di bawah ini:


 public function testUpdateCart() { Event::fake(); $cartService = resolve(CartService::class); $cartRepo = resolve(CartRepository::class); $user = factory(User::class)->make(); $cart = $cartRepo->getCart($user); // set data $data = [ ]; $newCart = $cartService->updateForUser($user, $data); $this->assertEquals($data, $newCart->toArray()); Event::assertDispatched(CartChanged::class, 1); } 

Event-Listener, Jobs


Kelas-kelas ini diuji hampir oleh prinsip umum - kami menyiapkan data yang diperlukan untuk pengujian; Kami memanggil kelas yang diinginkan dari framework dan memeriksa hasilnya.
Contoh untuk Pendengar:


 public function testHandle() { $user = factory(User::class)->create(); $cart = Cart::create([ 'userId' => $user->id, // other needed data ]); $listener = new CreateTaskForSyncCart(); $listener->handle(new CartChanged($cart)); $job = // get our job $this->assertSame(json_encode($cart->products), $job->payload); $this->assertSame($user->id, $job->user_id); // some additional asserts. Work with this data simplest for example $this->assertTrue($updatedAt->equalTo($job->last_updated_at)); } 

Konsol konsol


Saya menganggap perintah konsol sebagai beberapa jenis pengontrol yang dapat menghasilkan (dan melakukan manipulasi yang lebih kompleks dengan input-output konsol yang dijelaskan dalam dokumentasi) data. Dengan demikian, tes diperoleh mirip dengan controller: kami memeriksa bahwa metode layanan yang diperlukan dipanggil, peristiwa dipicu (atau tidak), dan kami juga memeriksa interaksi dengan konsol (output atau permintaan data).


Contoh tes serupa:


 public function testSendCartSyncDataEmptyJobs() { $service = m::mock(CartJobsRepository::class); app()->instance(CartJobsRepository::class, $service); $service->shouldReceive('getAll') ->once()->andReturn(collect([])); $this->artisan('sync:cart') ->expectsOutput('Get all jobs for sending...') ->expectsOutput('All count for sending: 0') ->expectsOutput('Empty jobs') ->assertExitCode(0); } 

Pisahkan perpustakaan eksternal


Sebagai aturan, jika pustaka terpisah memiliki fitur untuk pengujian unit, maka pustaka tersebut dijelaskan dalam dokumentasi. Dalam kasus lain, bekerja dengan kode ini diuji mirip dengan lapisan layanan. Tidak masuk akal untuk menutupi perpustakaan sendiri dengan tes (hanya jika Anda ingin mengirim PR ke perpustakaan ini) dan Anda harus menganggapnya sebagai semacam kotak hitam.


Pada banyak proyek, saya harus berinteraksi melalui API dengan layanan lain. Laravel sering menggunakan perpustakaan Guzzle untuk tujuan ini. Bagiku nyaman untuk menempatkan semua pekerjaan dengan layanan lain ke dalam kelas terpisah dari layanan NetworkService. Ini membuatnya lebih mudah bagi saya untuk menulis dan menguji kode utama, dan membantu untuk membakukan jawaban dan penanganan kesalahan.


Saya memberikan contoh beberapa tes untuk kelas NetworkService saya:


 public function testSuccessfulSendNetworkService() { $mockHandler = new MockHandler([ new Response(200), ]); $handler = HandlerStack::create($mockHandler); $client = new Client(['handler' => $handler]); app()->instance(\GuzzleHttp\Client::class, $client); $networkService = resolve(NetworkService::class); $response = $networkService->sendRequestToSite('GET', '/'); $this->assertEquals('200', $response->getStatusCode()); } public function testUnsupportedMethodSendNetworkService() { $networkService = resolve(NetworkService::class); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToSite('PUT', '/'); } public function testUnsetConfigUrlNetworkService() { $networkService = resolve(NetworkService::class); Config::shouldReceive('get') ->once() ->with('app.api_url') ->andReturn(''); Config::shouldReceive('get') ->once() ->with('app.api_token') ->andReturn('token'); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToApi('GET', '/'); } 

Kesimpulan


Pendekatan ini memungkinkan saya untuk menulis kode yang lebih baik dan lebih mudah dipahami, untuk mengambil keuntungan dari pendekatan SOLID dan SRP saat menulis kode. Tes saya menjadi lebih cepat, dan yang paling penting - mereka mulai menguntungkan saya.


Dengan refactoring aktif ketika memperluas atau mengubah fungsionalitas, kami segera melihat apa yang sebenarnya jatuh dan kami dapat dengan cepat dan akurat memperbaiki kesalahan tanpa melepaskannya dari lingkungan lokal. Ini membuat koreksi kesalahan semurah mungkin.


Saya berharap bahwa prinsip dan pendekatan yang saya jelaskan akan membantu Anda menangani pengujian unit di Laravel dan menjadikan unit test asisten Anda dalam pengembangan kode.


Tuliskan tambahan dan komentar Anda.

Source: https://habr.com/ru/post/id457866/


All Articles