Testing TDD trong Laravel P2 (Implementation Unit Test and Feature Test trong Laravel)
Xin chào các bạn, Tiếp tục với series về Testing TDD. Ở phần trước mình có giới thiệu qua về TDD, mục đích sử dụng và benefits cho project cũng như dự án thực tế. Bài viết này là phần 2 trong Testing TDD trong Laravel , mình sẽ chia sẻ cách implement TDD sử dụng Laravel Framework và phpunit . ...
Xin chào các bạn,
Tiếp tục với series về Testing TDD. Ở phần trước mình có giới thiệu qua về TDD, mục đích sử dụng và benefits cho project cũng như dự án thực tế. Bài viết này là phần 2 trong Testing TDD trong Laravel, mình sẽ chia sẻ cách implement TDD sử dụng Laravel Framework và phpunit.
Trước khi bắt đầu thì mình muốn recap ngắn gọn lại TDD để cho các bạn có một cái nhìn khái quát nhất.
Vậy TDD là gì ?
"TDD (Testing Driven Development) là một phương pháp tiếp cận sử dụng cho việc Testing, lập trình viên sẽ viết test trước khi code... Sau đó chạy test, tất nhiên lần đầu tiên test đó sẽ fail. Sau đó refactor lại code sao đó test đó pass."
Example
Để implement TDD cho Laravel, mình đưa ra một ví dụ đơn giản về Forum,
Mình có 3 table users, threads và channels. Dựa trên 3 table này, mình sẽ đi viết một số các Unit Test và Feature Test như: check validation, relationships giữa các table, authentication, create, delete, etc, ...
Setup Migrations, ModelFactory
Migrations
users table:
Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('email')->unique(); $table->string('password'); $table->rememberToken(); $table->timestamps(); });
threads table:
Schema::create('threads', function (Blueprint $table) { $table->increments('id'); $table->unsignedInteger('user_id'); $table->unsignedInteger('channel_id'); $table->string('title'); $table->text('body'); $table->timestamps(); });
channels table:
Schema::create('channels', function (Blueprint $table) { $table->increments('id'); $table->string('name', 50); $table->string('slug', 50); $table->timestamps(); });
Mối quan hệ (relationships) của 3 table trên:
User - Thread: One - Many (Một user có thể có nhiều thread).
Channel - Thread: One - Many (Một channel có thể có nhiều thread).
ModelFactory
<?php use FakerGenerator as Faker; /* |-------------------------------------------------------------------------- | Model Factories |-------------------------------------------------------------------------- | | This directory should contain each of the model factory definitions for | your application. Factories provide a convenient way to generate new | model instances for testing / seeding your application's database. | */ $factory->define(AppUser::class, function (Faker $faker) { return [ 'name' => $faker->name, 'email' => $faker->unique()->safeEmail, 'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret 'remember_token' => str_random(10), ]; }); $factory->define(AppThread::class, function (Faker $faker) { return [ 'user_id' => function () { return factory(AppUser::class)->create()->id; }, 'channel_id' => function () { return factory(AppChannel::class)->create()->id; }, 'title' => $faker->sentence, 'body' => $faker->paragraph, ]; }); $factory->define(AppChannel::class, function (Faker $faker) { return [ 'name' => $faker->word, 'slug' => $faker->word, ]; });
ModelFactory này sẽ được dùng để khởi tạo dữ liệu mẫu trong các test case, mình sẽ đề cập ở phần dưới.
Implementation
Sau khi setup xong Migrations và ModelFactory, mình sẽ đi vào chi tiết từng test case. Mình chia làm 2 loại test case, Unit Test và Feature Test.
Unit Test
Đối với Unit Test, loại test case này tập trung test những vấn đề basic nhất như: kiểm tra các trường (field) trong table có tồn tại trong bảng hay không, kiểm tra mối quan hệ của các bảng, ...
tests/Unit/ThreadTest.php
<?php namespace TestsUnit; use AppThread; use AppUser; use IlluminateFoundationTestingRefreshDatabase; use IlluminateSupportCollection; use TestsTestCase; class ThreadTest extends TestCase { use RefreshDatabase; protected $thread; public function setUp() { parent::setUp(); $this->thread = factory(Thread::class)->create(); } /** @test */ public function a_thread_has_creator() { $this->assertInstanceOf(User::class, $this->thread->creator); } /** @test */ public function a_thread_belongs_to_a_channel() { $thread = create(Thread::class); $this->assertInstanceOf('AppChannel', $thread->channel); } }
Thay vì mỗi Test Case, chúng ta phải khởi tạo 1 object thread sau đó sử dụng thì với Function setUp(), mình khởi tạo 1 object thread để tất cả các Test Case trong class ThreadTest đều có thể sử dụng chung.
factory(Thread::class)->create();
Hàm này sẽ gọi Thread ModelFactory mà mình đã nói ở trên để thực hiện việc khởi tạo Thread.
Ở đây mình có 2 test case.
Check thread thuộc về một tác giả và thuộc về một channel, kiểm tra xem thằng Thread có phải thuộc về một tác giả nào đó hay không bằng cách sử dụng assertInstanceOf (hàm này trong phpunit, các bạn có thể tìm hiểu thêm trong docs phpunit)
(Ví dụ: thread "Testing TDD Laravel" của anh Nguyen Van A, "TDD in Laravel" thuộc channel về Testing)
Oke done, vậy là xong phần viết test, tiếp theo là chạy test sử dụng command:
phpunit
Lúc này log sẽ show error đối với test case 1 và 2 lần lượt như sau:
Failed asserting that null is an instance of class "AppUser". Failed asserting that null is an instance of class "AppChannel".
Test case đầu tiên bao giờ cũng sẽ fail, bởi vì chúng ta mới chỉ khởi tạo table chứ chưa khởi tạo các mối quan hệ của chúng.
Bước tiếp theo là thiết tập mỗi quan hệ (relationships) giữa các table và làm sao cho 2 test case trên passed