Làm thế nào để test được một hàm là tham số của một hàm khác với Mockery trong PHP
TIL
770
PHP
96
Testing
30
White

Lê Mai Viện viết ngày 07/01/2020

Anonymous functions

Trong PHP có một khái niệm được dùng khá nhiều đặc biệt là với các Framework hiện đại như Laravel đó là hàm là tham số của một hàm khác hay còn gọi là Anonymous functions cũng thường được biết đến với cái tên Closure.

Ví dụ

Route::get('welcome', function() {
    return 'Welcome';
});

Hàm get ở trên có 2 tham số, tham số thứ nhất là 1 string còn tham số thứ 2 là một anonymous functions. Và vấn đề đặt ra là làm sao để test được cái hàm anonymous functions đó?

Mockery

Lại bắt đầu bằng một ví dụ

class PostRepository
{
    public function getPostsByUser(int $userId)
    {
        //return all posts of specific user
    }
}

class PostService
{
    private $postRepository;

    public function __construct(PostRepository $postRepository) {
        $this->postRepository = $postRepository;
    }

    public function getPostsByUser(int $userId)
    {
        return $this->postRepository->getPostsByUser($userId);
    }
}

Bây giờ chúng ta sẽ test PostService theo kiểu Unit Test, khác với Integration Test phải gọi thực sự cái method của dependency (là PostRepository), phải kết nối database các kiểu hoặc phải chạy qua nhiều hàm của nhiều class. Unit test chỉ đơn giản là đảm bảo method của dependency được gọi và phải được gọi đúng.

Code test sẽ như sau:

class PostServiceTest extends TestCase
{
    /** @var PostRepository|MockInterface */
    private $postRepository;

    /** @var PostService */
    private $postService;

    protected function setUp()
    {
        parent::setUp();
        $this->postRepository = Mockery::mock(PostRepository::class);
        $this->postService = new PostService($this->postRepository);
    }

    public function testGetPostsByUser()
    {
        $posts = [
            'post 1', 'post 2'
        ];
        $userId = 1;
        $this->postRepository->shouldReceive('getPostsByUser')
            ->with($userId)
            ->once()
            ->andReturn($posts);
        $this->assertEquals($posts, $this->postService->getPostsByUser($userId));
    }
}

Ở đây ta dùng thư viện Mockery để giả lập một mocked object. Trong ví dụ trên nhờ Mockery mà ta mong muốn method testGetPostsByUser của class PostService đảm bảo được những điều sau:

  • phải gọi method getPostsByUser của PostRepository
  • phải gọi đúng chính xác 1 lần
  • phải gọi với chính xác 1 tham số là $userId
  • Giả sử hàm của PostRepository trả về 1 cái abcxyz bất kì, thì hàm của PostService cũng phải trả về đúng chính xác cái đó.

Sau này lỡ vào một ngày mưa buồn rả rích, một thằng nào đó trong team táy máy vào thay đổi cái nội dung hàm trong PostService, nó muốn truyền thêm 1 tham số khác, hoặc muốn gọi hàm đó 69 lần thay vì chỉ gọi một lần... thì mình mong chờ cái test đó phải bị failed để còn biết đường mà tính. Hoặc là tới tát cho nó một tai vì dám thay đổi behaviour của hàm hoặc là năn nỉ nó lỡ làm con người ta có bầu (làm test fail) thì phải đi mà chịu trách nhiệm giải quyết (sửa lại cái test cho nó pass). Đơn giản rứa thôi.

Mock anonymos function

Đổi code lại một chút, PostService sẽ dùng thêm cả cache service để lấy dữ liệu post ra từ trong cache. Hàm getFromCache sẽ có 2 tham số, tham số thứ nhất là cache key, tham số thứ 2 là một Anonymous functions để lấy dữ liệu, nêu cacheService tìm không thấy dữ liệu bởi cache key thì sẽ gọi hàm này để lấy dữ liệu để lưu vào cache sau đó mới trả về. Code sẽ như sau:

class PostService
{
    private $postRepository;

    /** @var CacheInterface */
    private $cacheService;

    public function __construct(PostRepository $postRepository) {
        $this->postRepository = $postRepository;
    }

    public function getPostsByUser(int $userId)
    {
        $cacheKey = 'user-posts-' . $userId;
        return $this->cacheService->getFromCache($cacheKey, function () use ($userId) {
            return $this->postRepository->getPostsByUser($userId);
        });
    }
}

Giờ làm sao để test cái hàm getPostsByUser trong PostService?
Mockery có hỗ trợ cách để validate paramater với Mockery::on và tham số lại cũng là một anonymous functions.

Code test sẽ như sau:

class PostServiceTest extends TestCase
{
    /** @var PostRepository|MockInterface */
    private $postRepository;

    /** @var CacheInterface|MockInterface */
    private $cacheService;

    /** @var PostService */
    private $postService;

    protected function setUp()
    {
        parent::setUp();
        $this->postRepository = Mockery::mock(PostRepository::class);
        $this->cacheService = Mockery::mock(CacheInterface::class);
        $this->postService = new PostService($this->postRepository);
    }

    public function testGetPostsByUser()
    {
        $posts = [
            'post 1', 'post 2'
        ];
        $userId = 1;
        $this->postRepository->shouldReceive('getPostsByUser')
            ->with($userId)
            ->once()
            ->andReturn($posts);
        $this->cacheService->shouldReceive('getFromCache')
            ->with(
                'user-posts-' . $userId,
                Mockery::on(function ($closure) use ($posts){
                    return $closure() == $posts;
                })
            );
        $this->assertEquals($posts, $this->postService->getPostsByUser($userId));
    }
}

Ở đây ta đã mock PostRepository@ getPostsByUser để trả về một dữ liệu sample của posts, và hàm này lại được gọi trong Anonymous functions của CacheService, nên ta sẽ verify bằng cách khi gọi Anonymous functions này thì kết quả sẽ bằng đúng cái dữ liệu post đã mock ở trên.

http://docs.mockery.io/en/latest/reference/argument_validation.html

Mockery::on(function ($closure) use ($posts){
    return $closure() == $posts;
})

Và cuối cùng là assert dữ liệu trả về của PostService@getPostsByUser cũng trả về đúng kết quả như mong chờ

$this->assertEquals($posts, $this->postService->getPostsByUser($userId));

Hết

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Lê Mai Viện

1 bài viết.
0 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Bài viết liên quan
White
0 5
fCC: Technical Documentation Page note So I have finished the HTML part of this exercise and I want to come here to lament about the lengthy HTML ...
HungHayHo viết hơn 2 năm trước
0 5
White
6 0
1. Định nghĩa Một kế hoạch kiểm thử dự án phần mềm (test plan) là một tài liệu mô tả các mục tiêu, phạm vi, phương pháp tiếp cận, và tập trung vào...
Thiên Hoàng Minh Vũ viết gần 3 năm trước
6 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
1 bài viết.
0 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!