Nepgear.
Senior Member
Hello các bạn,
Từ khóa Unit Testing chắc hẳn ko lạ lẫm gì với tất cả chúng ta, especially những bạn làm ở vị trí Backend. Đây là 1 trong những thứ mà ai cũng muốn học, trải nghiệm và thậm chí apply vào dự án của các bạn. Rồi từ đó dần dần lên TDD, setup CI,...
Về advantages, đơn giản như sau:
Không phải application nào cũng cần tests, nhưng vẫn sẽ có các critical applications => có tests sẽ an tâm hơn.
Bài này mình hướng cụ thể vào PHP / Laravel, các bạn nào làm lang/fw khác vẫn có thể tham khảo các approaches nhé, vì dẫu sao chả là Backend tests
Nói không với các thể loại
Các loại tests ta có thể viết trong Laravel
P/s: đây là định nghĩa riêng của mình, nó ko như các định nghĩa chung chung như các online articles nhé
Assertions
Coverage?
Để viết tests cực kỳ lý tưởng và thành công, bạn cần:
Quick Test
Unit Test
Coming soon
Feature Test
Coming soon
CI chongười mới bắt đầu mọi người thông qua GitHub Actions
Coming soon
Upload Coverage Report lên CodeCov
Coming soon
Từ khóa Unit Testing chắc hẳn ko lạ lẫm gì với tất cả chúng ta, especially những bạn làm ở vị trí Backend. Đây là 1 trong những thứ mà ai cũng muốn học, trải nghiệm và thậm chí apply vào dự án của các bạn. Rồi từ đó dần dần lên TDD, setup CI,...
Về advantages, đơn giản như sau:
- Tăng độ tin cậy cho những gì bạn viết ra.
- Tránh dc những early-stage bugs.
- Có refactor thì cũng yên tâm ko hư hay thiếu vì đã có tests.
- Đi phỏng vấn mà biết viết test thì oai vkl tha hồ hét lương
- 98% các cty ở VN ko viết tests mà.
- ...
Bài này mình hướng cụ thể vào PHP / Laravel, các bạn nào làm lang/fw khác vẫn có thể tham khảo các approaches nhé, vì dẫu sao chả là Backend tests
Nói không với các thể loại
function calculate (a, b) { return a + b; }
rồi assertEquals(3, calculate(1, 2))
nhé, vì nó nhảm vkl và chả giúp ít dc gì.Các loại tests ta có thể viết trong Laravel
- Quick: gọi là Quick vì ta sẽ extend thẳng cái class TestCase của PHPUnit, ko cần phải bootup Laravel application lên
- Chạy cực nhanh
- Mocking data rồi run là chủ yếu - không thông qua thằng nào cả, kể cả database
- Test từng functions
- Unit: sẽ bootup Laravel application (services, facade,...) lên và sử dụng - có cả database
- Test đủ mọi thứ về business logic của application tại đây
- Test từng functions như Quick test
- Feature: tương tự như Unit
- Test HTTP request tới endpoints của application
- Để sure kèo với endpoint của bạn hoạt động đúng với data này và sai với data kia,...
- Test response data, status,...
- Test từng endpoints của Controller
- Test HTTP request tới endpoints của application
- Integration: tương tự như Feature, Integration dùng để test 1 chain of endpoints call theo Business Logic để xem nó hoạt động đúng ko
- Vd: tạo user, xong tiếp tục tạo Business, rồi tiếp tục tạo ABCXYZ,....
Assertions
Với PHPUnit, ta sẽ thông qua 1 đống assert methods để verify data và xác định test case của chúng ta chạy đúng ý ta muốn, từ return đúng cho tới return lỗi, return sai,...
Với Laravel, chúng ta còn có thêm 1 mớ assert methods khác, tìm hiểu thêm tại link: https://laravel.com/docs/8.x/testing hoặc bắt tay vào làm luôn là sẽ thấy
Với Laravel, chúng ta còn có thêm 1 mớ assert methods khác, tìm hiểu thêm tại link: https://laravel.com/docs/8.x/testing hoặc bắt tay vào làm luôn là sẽ thấy
Coverage?
Độ bao phủ - Coverage được thể hiện trong 1 method, đi vào ví dụ cho dễ hiểu:
Với method
Test:
Nhưng với method
Test 50%:
Lên 100%:
Cơ bản là khi chạy test suite + coverage driver, driver nó sẽ tính từng line mà nó đã executed rồi từ đó generate ra report.
Sau khi chạy hết test suites, ta sẽ biết dc overall coverage của project là bao nhiêu.
PHP có vài cái coverage driver như XDebug, Clover,... Mình thì đang xài Clover.
Với method
getAge
này, độ coverage khi bạn viết test sẽ là 100%:
PHP:
class User extends Model
{
public int $age;
public function getAge(): int
{
return $this->age;
}
}
Test:
PHP:
public function testGetAgeReturnsInteger()
{
$user = new User;
$user->age = 10;
$this->assertEquals(10, $user->getAge());
}
Nhưng với method
getSexText
này, có rẽ 1 nhánh if
, khi bạn test 1 case duy nhất thì coverage của bạn chỉ có 50%. Để đạt được 100%, bạn phải prepare data và test nốt result còn lại của if
. Tương tự như switch
PHP:
class User extends Model
{
public string $sex;
public function getSexText(): string
{
return $this->sex === 'M' ? 'Male' : 'Female';
}
}
Test 50%:
PHP:
public function testGetSexTextReturnsFemaleString()
{
$user = new User;
$this->assertEquals('Female', $user->getSexText());
}
Lên 100%:
PHP:
public function testGetSexTextReturnsMaleString()
{
$user = new User;
$user->sex = 'M';
$this->assertEquals('Male', $user->getSexText());
}
Cơ bản là khi chạy test suite + coverage driver, driver nó sẽ tính từng line mà nó đã executed rồi từ đó generate ra report.
Sau khi chạy hết test suites, ta sẽ biết dc overall coverage của project là bao nhiêu.
PHP có vài cái coverage driver như XDebug, Clover,... Mình thì đang xài Clover.
Để viết tests cực kỳ lý tưởng và thành công, bạn cần:
- Viết nhiều methods/functions, nó sẽ rất có ít cho việc mocking data
- Hạn chế dùng static methods, vì test cho bản thân nó thì có thể dc chứ dùng nó để mock cho 1 thằng khác depend vào thì ko được
- Sử dụng DI, Service Container mà Laravel đã cung cấp sẵn
- Tinh thần đạo đức cao, no tricks.
Quick Test
Ta sẽ dùng Quick Test chủ yếu là test relationship methods cũng như là getter của Eloquent:
Eloquent Model:
Test cases:
Ở trên các bạn có thể thấy mình sử dụng 1 helper để mock data -
Như trên thì mình expect cái method
P/s: bạn có thể 1000% yên tâm ko cần phải dùng Unit Test để get relationship data từ DB ra, vì dưới Eloquent ng` ta đã viết full test + cover hết cho bạn rồi.
Eloquent Model:
PHP:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Article extends Model
{
protected $table = 'articles';
protected $fillable = [
'user_id',
'title',
'content',
];
protected $casts = [
'user_id' => 'int',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(
Tag::class,
'article_tag',
'article_id',
'tag_id'
);
}
public function getUserName(): string
{
return $this->user->getFullName();
}
}
Test cases:
PHP:
namespace Tests\Quick\Models;
use App\Models\Article;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class ArticleTest extends TestCase
{
public function testArticleBelongsToUser()
{
$article = new Article();
$this->assertInstanceOf(BelongsTo::class, $article->user());
}
public function testArticleBelongsToManyTags()
{
$article = new Article();
$this->assertInstanceOf(BelongsToMany::class, $article->tags());
}
public function testGetUserNameReturnsNameOfPoster()
{
$article = new Article();
$article->setRelation('user', $user => $this->createMock(User::class));
$user->expects($this->once())
->method('getFullName')
->willReturn('Arthur Morgan');
$this->assertEquals('Arthur Morgan', $article->getUserName());
}
}
Ở trên các bạn có thể thấy mình sử dụng 1 helper để mock data -
createMock
method của PHPUnit, PHPUnit sẽ tạo ra 1 object và bạn có thể set data trả về cho nó cũng như là expects nó run bao nhiêu lần (có cả validate params đầu vào).Như trên thì mình expect cái method
getFullName
sẽ dc invoke 1 lần và return Authur Morgan
. Chúng ta sẽ đi tiếp vào cái này nhiều hơn ở mục nâng cao nhé.P/s: bạn có thể 1000% yên tâm ko cần phải dùng Unit Test để get relationship data từ DB ra, vì dưới Eloquent ng` ta đã viết full test + cover hết cho bạn rồi.
Coming soon
Unit Test
Coming soon
Feature Test
Coming soon
CI cho
Coming soon
Upload Coverage Report lên CodeCov
Coming soon