0
0
Lập trình
TT

Module 4: Khám Phá Test Doubles (Mocks và Stubs)

Đăng vào 5 ngày trước

• 8 phút đọc

Giới thiệu

Chào mừng bạn đến với module sẽ thay đổi cách bạn viết unit tests. Từ trước đến nay, chúng ta đã kiểm tra các lớp độc lập. Tuy nhiên, trong thực tế, các lớp thường phải hợp tác với nhau. Ví dụ, một lớp UserService có thể cần một lớp EmailClient để gửi email, hoặc một Logger để ghi lại các hoạt động.

Làm thế nào chúng ta có thể kiểm tra logic của UserService mà không thực sự gửi email hoặc ghi vào tệp log mỗi khi bài kiểm tra chạy? Câu trả lời nằm ở Test Doubles, một khái niệm cơ bản cho việc tách biệt kiểm tra.

1. Test Doubles là gì?

Test Double là thuật ngữ chung cho bất kỳ đối tượng nào đóng giả một đối tượng khác nhằm mục đích kiểm tra. Hãy nghĩ về nó như một diễn viên đóng thế trong phim: họ thay thế diễn viên chính trong những tình huống cụ thể, cho phép cảnh quay được thực hiện an toàn và có kiểm soát.

Trong trường hợp của chúng ta, chúng ta sử dụng doubles để thay thế các phụ thuộc của lớp mà chúng ta đang kiểm tra (gọi là "Unit Under Test"). Điều này cho phép chúng ta:

  • Tách biệt Unit Under Test: Chúng ta đảm bảo rằng bài kiểm tra chỉ thất bại nếu có vấn đề trong lớp mà chúng ta đang kiểm tra, không phải trong các phụ thuộc của nó.
  • Kiểm soát Môi trường: Chúng ta có thể buộc các phụ thuộc cư xử theo những cách cụ thể (ví dụ: mô phỏng một lỗi kết nối cơ sở dữ liệu) để kiểm tra tất cả các nhánh của mã.
  • Tăng tốc độ Kiểm tra: Thay thế một phụ thuộc chậm (như một cuộc gọi mạng) bằng một double trong bộ nhớ giúp quá trình thực thi kiểm tra nhanh hơn nhiều.

Hai loại Test Doubles phổ biến nhất mà bạn sẽ sử dụng với PHPUnit là StubsMocks.

2. Stubs: Mô phỏng Hành vi (Kiểm soát Trạng thái)

Một Stub là một double cung cấp các "câu trả lời có sẵn" cho các lời gọi phương thức trong quá trình kiểm tra. Mục đích chính của một stub là đảm bảo rằng đơn vị dưới thử nghiệm nhận được dữ liệu cần thiết để thực hiện công việc của nó, cho phép bài kiểm tra tiếp tục.

Khi nào nên sử dụng Stub?

Khi bạn cần mô phỏng trạng thái của một phụ thuộc.

Ví dụ Thực tế:

Giả sử chúng ta có một lớp WelcomeGenerator phụ thuộc vào một lớp Translator để lấy lời chào chính xác.

php Copy
// src/Translator.php
interface Translator
{
    public function getGreeting(): string;
}

// src/WelcomeGenerator.php
class WelcomeGenerator
{
    private $translator;

    public function __construct(Translator $translator)
    {
        $this->translator = $translator;
    }

    public function greet(string $name): string
    {
        $greeting = $this->translator->getGreeting();
        return "{$greeting}, {$name}!";
    }
}

Để kiểm tra lớp WelcomeGenerator, chúng ta không muốn phụ thuộc vào một triển khai thực tế của Translator. Thay vào đó, chúng ta tạo một stub.

php Copy
// tests/WelcomeGeneratorTest.php
use PHPUnit\Framework\TestCase;

class WelcomeGeneratorTest extends TestCase
{
    public function testGreetingInEnglish()
    {
        // 1. Tạo một stub cho interface Translator
        $translatorStub = $this->createStub(Translator::class);

        // 2. Cấu hình stub: Khi phương thức 'getGreeting' được gọi,
        //    nó sẽ trả về chuỗi 'Hello'.
        $translatorStub->method('getGreeting')->willReturn('Hello');

        // 3. Tiêm stub vào lớp đang thử nghiệm
        $generator = new WelcomeGenerator($translatorStub);

        // 4. Thực hiện và xác minh
        $result = $generator->greet('John');
        $this->assertEquals('Hello, John!', $result);
    }
}

Trong bài kiểm tra này, không quan trọng cách mà translator hoạt động. Chúng ta chỉ quan tâm rằng nó cung cấp cho chúng ta chuỗi "Hello" để kiểm tra rằng WelcomeGenerator đã nối đúng với tên.

3. Mocks: Xác minh Tương tác (Xác minh Hành vi)

Một Mock là một double "thông minh" hơn. Giống như stub, nó có thể trả về giá trị, nhưng mục đích chính của nó là xác minh rằng một số phương thức đã được gọi trên phụ thuộc. Mocks được sử dụng để kiểm tra sự tương tác giữa các đối tượng.

Khi nào nên sử dụng Mock?

Khi bạn cần xác minh rằng lớp dưới thử nghiệm đang gọi đúng phương thức của các phụ thuộc của nó.

Ví dụ Thực tế:

Giả sử chúng ta có một lớp UserRegistrar mà sau khi lưu người dùng, nó phải thông báo cho dịch vụ Logger.

php Copy
// src/Logger.php
interface Logger
{
    public function log(string $message): void;
}

// src/UserRegistrar.php
class UserRegistrar
{
    private $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function register(string $name)
    {
        // Logic để lưu người dùng vào cơ sở dữ liệu...
        echo "Đang lưu người dùng {$name}...\n";

        // Thông báo cho logger
        $this->logger->log("Người dùng {$name} đã đăng ký thành công.");
    }
}

Bài kiểm tra cần đảm bảo rằng phương thức log của Logger được gọi.

php Copy
// tests/UserRegistrarTest.php
use PHPUnit\Framework\TestCase;

class UserRegistrarTest extends TestCase
{
    public function testShouldLogMessageOnRegister()
    {
        // 1. Tạo một mock cho interface Logger
        $loggerMock = $this->createMock(Logger::class);

        // 2. Thiết lập kỳ vọng: Chúng ta mong đợi phương thức 'log'
        //    sẽ được gọi NÓI CHUNG MỘT LẦN.
        $loggerMock->expects($this->once())
                   ->method('log')
                   ->with($this->equalTo('Người dùng Alice đã đăng ký thành công.'));

        // 3. Tiêm mock vào lớp đang thử nghiệm
        $registrar = new UserRegistrar($loggerMock);

        // 4. Thực hiện phương thức
        $registrar->register('Alice');
    }
}

Phân tích Mock:

  • createMock(): Tạo đối tượng mock.
  • expects($this->once()): Đây là kỳ vọng. Chúng ta đang nói với PHPUnit: "Tôi mong rằng trong quá trình thực hiện bài kiểm tra này, phương thức sau sẽ được gọi đúng một lần." Các lựa chọn khác bao gồm any(), never(), atLeastOnce().
  • method('log'): Xác định phương thức mà chúng ta đang theo dõi.
  • with(...): (Tùy chọn) Xác định các tham số chính xác mà chúng ta mong đợi phương thức sẽ được gọi.

Nếu phương thức log không được gọi, hoặc được gọi nhiều hơn một lần, hoặc được gọi với một thông điệp khác, bài kiểm tra sẽ thất bại, ngay cả khi không có assert rõ ràng ở cuối. Kỳ vọng (expects) tự nó là một khẳng định.

4. Sự khác biệt quan trọng: Mocks vs. Stubs

Đặc điểm Stub Mock
Mục đích chính Cung cấp câu trả lời có sẵn (trạng thái) Xác minh tương tác (hành vi)
Tiêu điểm kiểm tra Trên trạng thái cuối cùng của đối tượng đang kiểm tra Trên sự giao tiếp giữa đối tượng kiểm tra và phụ thuộc
Xác minh Bài kiểm tra thực hiện các khẳng định chống lại lớp đang kiểm tra Bài kiểm tra xác minh rằng mock đã được sử dụng như mong đợi (expects)
Phép ẩn dụ Một diễn viên với kịch bản cố định Một diễn viên được giám sát bởi một đạo diễn kiểm tra hành động của họ

5. Những Doubles khác: Fakes và Spies

Mặc dù Mocks và Stubs là phổ biến nhất, nhưng cũng đáng để biết hai loại khác:

  • Fakes: Đây là những đối tượng có một triển khai hoạt động, nhưng đơn giản hơn nhiều so với phiên bản sản xuất. Một ví dụ cổ điển là cơ sở dữ liệu trong bộ nhớ thay thế một kết nối thực tế đến MySQL hoặc PostgreSQL. Nó hoạt động, nhưng được đơn giản hóa cho mục đích kiểm tra.
  • Spies: Một spy là một double "theo dõi" các lời gọi phương thức mà không can thiệp vào chúng (trừ khi được chỉ định). Sau khi mã được thực thi, bạn có thể thực hiện các khẳng định về cách mà spy đã được sử dụng. Trong PHPUnit, bạn thường có thể đạt được hành vi giống như spy bằng cách sử dụng một mock với kỳ vọng linh hoạt.

Thực hành tốt nhất

  • Sử dụng Stubs và Mocks một cách hợp lý: Chỉ sử dụng stub khi bạn cần mô phỏng trạng thái và mock khi bạn cần xác minh tương tác.
  • Giữ cho bài kiểm tra ngắn gọn và rõ ràng: Mỗi bài kiểm tra nên kiểm tra một điều duy nhất để dễ dàng xác định lỗi.
  • Cập nhật bài kiểm tra khi có thay đổi: Đảm bảo rằng các bài kiểm tra của bạn luôn phản ánh logic hiện tại của mã.

Kết luận

Việc làm chủ Stubs và Mocks là một bước nhảy vọt lớn trong khả năng viết các bài kiểm tra unit hiệu quả và thực sự "unit". Trong module tiếp theo và cuối cùng, chúng ta sẽ tổng hợp tất cả kiến thức bạn đã tích lũy và khám phá các chủ đề nâng cao như phân tích độ bao phủ mã, tích hợp bài kiểm tra của bạn vào quy trình CI/CD, và các thực hành tốt nhất để đảm bảo bộ kiểm tra của bạn là một tài sản quý giá cho dự án của bạn.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào