Viết test sao cho đúng
Chào mọi người, mình hiện tại đang muốn viết test. Nhưng đang gặp vấn đề vì không biết test như nào cho chặt chẽ. Mình có ví dụ sau. Mình đang viết test cho 1 hàm trong repository
/**
* @inheritdoc
*/
public function getPermission()
{
return $this->model->orderBy('updated_at', 'DESC')->get();
}
Đây là đoạn mình viết test case
<?php
namespace Tests\Unit\RepositoryTest;
use DB;
use Mockery;
use Tests\TestCase;
use App\Models\Permission;
use Tests\SeedDataTrait;
use App\Repositories\PermissionRepository;
use Illuminate\Support\Collection as Collection;
class PermissionRepositoryTest extends TestCase
{
use SeedDataTrait;
/**
* Setup
*
* @param mixed $modelPermission
*
* @return void
*/
public function setUp()
{
parent::setUp();
$this->modelPermission = Mockery::mock(Permission::class);
$this->repository = new PermissionRepository(
$this->modelPermission
);
}
/**
* Test get permission
*
* @return void
*/
public function testGetPermission()
{
$permission = factory(Permission::class)->create();
$queryPermission = Permission::query();
$this->modelPermission->shouldReceive('orderBy')->andReturn($queryPermission);
$this->modelPermission->shouldReceive('get')->andReturn($permission);
$result = $this->repository->getPermission();
$this->assertInstanceOf(Collection::class, $result);
$this->assertArrayHasKey('name', $result[0]);
}
}
Ở đây mình tư duy test như sau
- Hàm
orderBy
trả về 1 QueryBuilder nên mình tao 1 seeder là 1 QueryBuilder - Hàm
get
trả về 1 collecttion nên mình tạo 1 collection rồi return khi shouldReceive
Thắc mắc của mình là
- Đây là test kiểu Unit Test hay Integration Test ? Làm sao để phân biệt giữa 2 cái này ?
- Ở đây mình muốn test chặt chẽ hơn 1 chút, cụ thể là test đối với câu truy vấn SQL thuần thì phải thiết kế test như thế nào ? Tức là mình muốn so sách câu SQL
select * from table order by updated_at as DESC
thuần với câu truy vấn hiện tại của mình xem có khớp hay không ?
Rất vui khi được trao đổi với mọi người, Ai có examples test chuẩn có thể cho mình xin luôn ạ. Cảm ơn mọi người
1 CÂU TRẢ LỜI
Đây là test kiểu Unit Test hay Integration Test ? Làm sao để phân biệt giữa 2 cái này ?
Bạn có thể tham khảo thêm về vấn đề này ở đây
Hiểu đơn giản thì Unit Test là quá trình mà ở đó, tất cả các Test Case cho từng function/method riêng biệt được viết, còn Integration Test là công đoạn trong kiểm thử phần mềm mà ở đó, từng đơn vị phần mềm riêng biệt được kết hợp lại, và kiểm tra như một khối thống nhất.
Một function test của bạn sẽ là Integration test nếu nó thực hiện một trong những việc sau đây:
- Trigger để chạy codes từ những functions khác trong project (từ function này gọi đến, và rồi chạy code ở function khác trong cùng project)
- Truy vấn vào cơ sở dữ liệu
- Sử dụng file system
- Truy cập mạng
- ...
Ở đây mình muốn test chặt chẽ hơn 1 chút, cụ thể là test đối với câu truy vấn SQL thuần thì phải thiết kế test như thế nào?
Bạn có thể dùng hàm DB::enableQueryLog()
để lưu lại log của câu truy vấn. Sau khi gọi hàm của model xong, bạn dùng DB::getQueryLog()
thì sẽ lấy được nội dung câu sql
mà bạn vừa thưc hiện.
Đấy là cách viết đơn giản, theo kiểu integration test, chứ muốn viết theo kiểu unit test thì nó sẽ khó khăn hơn nhiều, khi bạn sẽ phải mock cả database connection, ngăn việc truy vấn database thực sự xảy ra Bạn có thể tham khảo cách viết này.
Ai có examples test chuẩn có thể cho mình xin luôn ạ
Không dám nói là chuẩn, nhưng bạn có thể xem qua project này https://github.com/framgia/laravel-test-examples . Mặc dù được viết cách đây 2 năm rồi, hiện cũng không còn được maintain mấy nữa, nhưng hy vọng có thể giúp ích đôi chút cho bạn (^^;)
Đoạn code như ở repository ở trên theo anh nên test như thế nào cho hợp lý ạ :-? Phần ở trên đang dùng model để viết repo thì test truy vấn sql sẽ ntn ạ :-?
@thangtd90 Em cảm ơn anh vì câu trả lời ạ, Em cũng đang follow theo laravel test của framgia. Nhưng trong phần repository đang thiết kế khác em một chút nên đang k follow theo test repository trong đó được.
Cụ thể là ở đây em inject trực tiếp Model vào để sử dụng Eloquent còn ví dụ của Framgia đang dùng Query\Builder
<?php
namespace App\Repositories;
use App\Models\Permission;
use App\Repositories\BaseRepository;
use App\Repositories\Contracts\PermissionRepositoryInterface;
/**
* Class RoleRepository
*
* @package App\Repositories
*/
class PermissionRepository extends BaseRepository implements PermissionRepositoryInterface
{
/**
* @var Permission
*/
protected $model;
/**
* RoleRepository constructor.
*
* @param Permission $model
*/
public function __construct(Permission $model)
{
parent::__construct($model);
}
}
Nếu coi unit test em viết hiện tại là Bad Practices thì sửa như thế nào để nó thành Good Practices anh nhỉ.
@Naem @jquery123 Như anh đã chia sẻ ở trên thì để viết được Unit Test cho phần mà có liên quan đến Model rất là khó, đặc biệt là mọi người yêu cầu test cả các hàm mà có query Database. Để thực hiện được điều đó thì đòi hỏi các em phải mock cả Database connection cơ.
Cái project https://github.com/framgia/laravel-test-examples/ này là trước đây một người bạn làm cùng với anh viết ra. Mấy cái đoạn mock Database connection để viết unit test cho những phần liên quan đến Model cũng là do anh ý thiết kế, thật sự rất hay và đáng bái phúc
Nếu em đang thiết kế repo theo kiểu là một tầng ở phía trước Model như hiện tại, và muốn test những query gọi từ model như vậy thì em có thể tham khảo đoạn này https://github.com/framgia/laravel-test-examples/blob/master/tests/Unit/Http/Controllers/CityControllerTest.php#L32 . Đây là phần viết Unit Test cho Controller, có các đoạn test cho các hàm CRUD ở Controller, trong đó có dùng model đấy. Hơi phức tạp một chút, giờ anh đọc lại cũng không hiểu lắm =)) Nhìn chung là flow là mock database connection, chạy hàm cần test, và expect rằng đã có câu lệnh select
, update
, create
hay delete
được gọi, với arguments cụ thể là gì, và kết quả sẽ ra sao.
P/S: Đoạn test ban đầu mà @jquery123 có viết thì cũng rất gần với khái niệm Unit Test rồi, tuy nhiên nó vẫn chưa phải là một hàm test tốt, khi mà em chỉ test được là hàm orderBy
với get
sẽ được gọi thôi. Chứ nội dung kết quả của hàm get
thì em chưa test được. Giả sử như đoạn test của em đang pass
, giờ anh vào sửa code trong repository, đoạn $this->model->orderBy('updated_at', 'DESC')->get();
, anh sửa DESC
thành ASC
, hoặc gọi thêm một vài hàm nữa như limit
chẳng hạn, thì nó vẫn tiếp tục pass
@thangtd90 Anh vừa nhắc em mới nhớ thêm 1 vấn đề nữa khi viết Test. Như em hiểu thì mình sẽ viết test dựa vào spec dự án đưa ra nhưng ở đây hình như mình đang test theo hướng dựa vào logic code
, dẫn tới việc khi sửa code đồng nghĩa với việc sửa test, Vây có phải mình đã đi sai đường chăng ?
Rất mong được trao đổi với anh và mọi người
@jquery123
Ở đây mình muốn test chặt chẽ hơn 1 chút, cụ thể là test đối với câu truy vấn SQL thuần thì phải thiết kế test như thế nào ? Tức là mình muốn so sách câu SQL select * from table order by updated_at as DESC thuần với câu truy vấn hiện tại của mình xem có khớp hay không ?
Bản thân mục đích test của em đang bị chệch hướng rồi, bản thân eloquent lẫn mysql đều là các interface/ và implementation có sẵn, có nghĩa là em không có trách nhiệm test chúng nó
Với cùng một đoạn code, không phải cứ áp dụng được nhiều kỹ thuật test vào là nó sẽ trở nên "tốt". Tùy vào mục đích, chi phí và hàng tá các tiêu chí khác mà bạn có thể áp dụng kỹ thuật test phù hợp.
Chỉ xét riêng trong ví dụ này, tôi sẽ không viết unit test cho repository, hãy sử dụng integration test cho phần này.
1 cái nữa là em đang test repository nhưng trong test em lại dùng eloquent làm active record
good practice :
- mock database connection (ví dụ in-memory chẳng hạn) -> mock kết quả trả về từ hàm model->orderBy('updated_at', 'DESC')->get();
- spy hàm orderBy('updated_at', 'DESC') ,đặt các expect cho nó được gọi mấy lần và những tham số có như mình kỳ vọng không
- so sánh kết quả trả về từ repository với kỳ vọng của mình
-> bản thân model là các library dùng sẵn nên mình chỉ nên dùng nó như 1 spy chứ không nên mock , vì sao ? bởi vì nghiễm nhiên model nhận đúng tham số thì nó phải trả về 1 kết quả đúng -> không đúng thì đi kiện thằng viết library nhé
Tư duy của mình nghĩ ở đây là: