Một ví dụ TDD đơn giản trong Laravel
Chào các bạn! Hôm nay mình xin giới thiệu với các bạn về một ví dụ đơn giản sử dụng TDD trong Laravel. TDD là gì và tại sao cần sử dụng nó chắc hẳn mọi người đã ít nhiều được nghe đến cho nên mình sẽ không đề cấp tiếp ở đây mà mình sẽ chỉ chú trọng vào ví dụ sử dụng TDD đơn giản để mọi người có thể hiểu qua về cách viết TDD. Giờ thì BẮT ĐẦU THÔI !!!
Đây là một ví dụ TDD cho ứng dụng Laravel cung cấp các API quản lý các tác vụ, người dùng có thể thêm, xóa các tác vụ khị cần thiết.
Cấu hình testing database
Cập nhật file phpunit.xml như sau:
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
<env name="API_DEBUG" value="false"/>
<ini name="memory_limit" value="512M" />
<server name="MAIL_MAILER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/>
</php>
Chúng ta sẽ sử dụng testing database sqlite thay vì database thực tế để tesr và chạy test trong bộ nhớ. Bạn phải đặt debug thành false vì chúng ta chỉ cần các lỗi thực tế để xác nhận. Tăng giới hạn bộ nhớ có thể cần thiết trong tương lai khi thực hiện các test trở nên tốn bộ nhớ.
Giờ hãy chạy thử các Test bằng câu lệnh php artisan test
. Sẽ được kết quả như sau:
Ta sẽ thấy 2 file ExampleTest đó là 2 file Test có sẵn sau khi mình tạo project Laravel
Cấu hình file TestCase cơ bản như sau
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Faker\Factory as Faker;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication, DatabaseMigrations, DatabaseTransactions;
protected $faker;
/**
* Set up the test
*/
public function setUp(): void
{
parent::setUp();
$this->faker = Faker::create();
}
/**
* Reset the migrations
*/
public function tearDown(): void
{
$this->artisan('migrate:reset');
parent::tearDown();
}
}
Chúng ta cần thêm trait DatabaseMigations , vì vậy trong mỗi lần chạy thử nghiệm, các file migration cũng đang được chạy. Phương thức setUp () dùng để tạo ra data test còn phương thức tearDown() dùng để bỏ đi các dữ liệu test vừa được tạo ra khi test chạy xong.
Viết Test.
Như đã đề cập bên đây là ứng dụng Laravel cung cấp API quản lý các tác vụ, người dùng có thể thêm, xóa các tác vụ khị cần thiết.
Nó dự kiến bao gồm các API như sau:
/api/task/create
chấp nhận một request POST để tạo task
/api/task {id}complete
đánh dấu một task là đã hoàn thành
/api/task/{id}/delete
xóa task khỏi database
Giờ thì viết Test để đảm bảo các API chạy đúng.
Tạo một file feature test ApiTest.php
bằng câu lệnh `php artisan make:test ApiTest
Test API tạo Task.
Trong file ApiTest.php vừa tạo thay hàm test_example mặc định bằng hàm sau:
public function test_that_a_task_can_be_added()
{
$this->withoutExceptionHandling();
$response = $this->post('/api/tasks/create', [
'name' => 'Write Blog TDD',
'description' => 'Write and publish an blog TDD'
]);
$response->assertStatus(201);
$this->assertTrue(count(Task::all()) > 0);
}
Giải thích:
$ this->withoutExceptionHandling () yêu cầu PHPUnit không xử lý các ngoại lệ mà chúng ta có thể nhận được. Thao tác này là vô hiệu hóa xử lý ngoại lệ của Laravel để ngăn Laravel xử lý các ngoại lệ xảy ra thay vì ném nó đi, làm điều này để có thể nhận được báo cáo lỗi chi tiết hơn trong đầu ra thử nghiệm của mình.
$response->assertStatus(201) xác nhận rằng mã trạng thái HTTP được trả về từ request post là tài nguyên được tạo.
$this->assertTrue() để xác nhận rằng tác vụ đã thực sự được lưu trong cơ sở dữ liệu.
Sau đó tiến hành chạy Test bằng câu lệnh php artisan test
thu được kết quả:
Chúng ta thấy rằng test của chúng ta chạy chưa thành công vì một request POST tới đường dẫn http://localhost/api/tasks/create
mà đường dẫn này chưa tồn tại => Nó gửi ra ngoại lệ
Để xử lý ta cần đăng ký các đường dẫn trong file api.php như sau:
Route::group(['prefix' => 'tasks'], function () {
Route::get('/{task}', [TaskApiController::class, 'show']);
Route::post('/create', [TaskApiController::class, 'store']);
Route::patch('{task}/complete',[TaskApiController::class, 'mark_task_as_completed']);
Route::delete('/{id}', [TaskApiController::class, 'destroy']);
});
Sau đó chạy lại test và tiếp tục thu được lỗi như sau:
Test tiếp tục fail là do ta chưa có class TaskApiController. Để sửa ta sẽ tạo một resourse controller như sau:
php artisan make:controller TaskApiController --resource
Tiếp tục fail test là do mình chưa có class Task. Cần tạo tiếp một model Task và một file migration create_tasks_table bằng câu lệnh: php artisan make:model Task -m
Trong file Task.php như sau:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
use HasFactory;
protected $fillable = ['name', 'description', 'completed'];
}
Trong file migration create_tasks_table như sau:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTasksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description');
$table->boolean('completed')->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('tasks');
}
}
Chạy lai Test một lần nữa và ta thấy test đã chạy thành công
Test API chỉnh sửa Task (đánh dấu là đã hoàn thành)
Ta sẽ thêm một đoạn code test sau vào ApiTest.php
public function test_that_a_task_can_be_completed()
{
$this->withoutExceptionHandling();
$task_id = Task::create([
'name' => 'Write Blog TDD',
'description' => 'Is completed'
])->id; // create a task and store the id in the $task_id variable
$response = $this->patch("/api/tasks/$task_id/complete"); //sends a patch request in order to complete the created task
$this->assertTrue(Task::findOrFail($task_id)->is_completed() == 1); // assert that the task is now marked as completed in the database
$response->assertJson([
'message' => 'task successfully marked as updated'
], true); // ensure that the JSON response recieved contains the message specified
$response->assertStatus(200); // furthe ensures that a 200 response code is recieved from the patch request
}
Sau đó chạy Test thu được:
Chắc chắn là test sẽ fail và tương tư như phần test API tạo task ta sẽ fix từng lỗi một. Tổng hợp lại ta sẽ cần sửa file sau:
Thứ nhất là TaskApiController.php
public function mark_task_as_completed($id)
{
$task = (Task::findOrFail($id));
$task->mark_task_as_completed(); //calls the mark_task_as_complete method we created in the App/Models/Task.php file
return response()->json([
'message' => 'task successfully marked as updated'
], 200); //send a json response with the 200 status code
}
Thứ hai là Task.php
public function mark_task_as_completed()
{
$this->completed = 1;
$this->save();
}
public function is_completed()
{
return $this->completed;
}
Sau đó chạy lại Test và ta thấy test đã chạy thành công
Test API xóa task
Ta thêm đoạn code Test sau vào ApiTest để test chức năng xóa task
public function mark_task_as_completed()
{
$this->completed = 1;
$this->save();
}
public function is_completed()
{
return $this->completed;
}
Tương tự như 2 phần trên ta sẽ cần chạy Test fail, sau đó báo lỗi ở đâu ta sửa dần ở đó
Các file cần chỉnh sửa, thêm mới là: TaskFactory, TaskApiController. cụ thể
Thêm mới file TaskFactory
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class TaskFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->sentence(4),
'description' => $this->faker->sentence(20),
'completed' => random_int(0, 1)
];
}
}
Thêm code trong file TaskApiController
public function destroy($id)
{
$task_to_be_deleted = Task::findOrFail($id);
$task_to_be_deleted->delete();
return response()->json([
'message' => 'task deleted successfully'
], 410);
}
Cuối cùng là tất cả Test đã chạy thành công
Bài viết tới đây đã khá dài. Mình hi vọng với bài viết này sẽ giúp các bạn có thể hiểu hơn về tư tương viết TDD trong Laravel nói riêng và toàn bộ ngôn ngữ, framework khác nói chung.
Cảm ơn mọi người đã đọc!!!
THAM KHẢO: