kiến thức Bắt đầu với Unit Testing (PHP / Laravel)

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:
  • 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ù lại thì tốn time vkl
uq1dgnk.png
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
MjfezZB.png


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
  • 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,....
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é
meoqQpA.png


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
uq1dgnk.png

Coverage?
Độ bao phủ - Coverage được thể hiện trong 1 method, đi vào ví dụ cho dễ hiểu:

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:
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 ngườ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
 
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:
  • 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ù lại thì tốn time vkl
uq1dgnk.png
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
MjfezZB.png


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
  • 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,....
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é
meoqQpA.png


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
uq1dgnk.png

Coverage?
Độ bao phủ - Coverage được thể hiện trong 1 method, đi vào ví dụ cho dễ hiểu:

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:
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 ngườ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
Ngon. Cái này hay nè.
 
Viết test nhiều sẽ tập dần thói quen sử dụng DI, tránh dùng mấy hàm global "lười biếng" của laravel như dispatch, auth... :big_smile: Mọi thứ đều phải được "tiêm" vào một cách tường minh
 
Viết test nhiều sẽ tập dần thói quen sử dụng DI, tránh dùng mấy hàm global "lười biếng" của laravel như dispatch, auth... :big_smile: Mọi thứ đều phải được "tiêm" vào một cách tường minh
Chuẩn đấy fen
zFNuZTA.png
 
Back
Top