Cấu hình PHPUnit tối ưu theo nguyên tắc Clean Code cho dự án PHP

Việc thiết lập môi trường kiểm thử PHPUnit không chỉ dừng lại ở việc chạy được các test case — mà còn là quá trình xây dựng một hệ thống kiểm thử bền vững, dễ đọc và dễ mở rộng. Dưới góc nhìn của nguyên tắc Clean Code, mỗi dòng mã trong file test đều phải truyền tải rõ ràng mục đích, hành vi mong đợi và ranh giới trách nhiệm.

Tại sao cấu hình PHPUnit cần tuân thủ Clean Code?

Test code không phải "code phụ" — nó là tài liệu sống mô tả hành vi hệ thống. Khi cấu hình và viết test theo chuẩn Clean Code, bạn đạt được:

  • Tính minh bạch: Cấu trúc thư mục, tên lớp và phương thức phản ánh đúng ngữ cảnh kiểm thử (ví dụ: UserRegistrationFlowTest thay vì Test001)
  • Tính tách biệt: Mỗi suite kiểm thử có mục tiêu rõ ràng (unit, integration, contract), tránh chồng chéo và phụ thuộc ngầm
  • Tính tái sử dụng: Các phần tử như bootstrap, data providers hoặc base test classes được tổ chức theo nguyên tắc DRY và SRP

Cài đặt và cấu hình cơ bản

Dùng Composer để cài đặt phiên bản ổn định nhất:

composer require --dev phpunit/phpunit:^10.5

Tạo tệp phpunit.xml với cấu hình tối ưu:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
    bootstrap="tests/Bootstrap.php"
    colors="true"
    verbose="false"
    cacheDirectory=".phpunit.cache"
>
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
            <exclude>tests/Integration</exclude>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory suffix=".php">src</directory>
        </include>
    </source>

    <logging>
        <log type="coverage-html" target="build/coverage" lowUpperBound="50" highLowerBound="90"/>
        <log type="junit" target="build/logs/junit.xml"/>
    </logging>
</phpunit>

Tổ chức thư mục theo trách nhiệm rõ ràng

Cấu trúc đề xuất giúp phân tách mức độ kiểm thử và giảm độ phức tạp khi bảo trì:

tests/
├── Bootstrap.php              # Khởi tạo môi trường (autoload, mocks toàn cục)
├── Unit/
│   ├── Domain/
│   │   ├── UserCreationTest.php
│   │   └── ProductPricingTest.php
│   └── Infrastructure/
│       └── InMemoryUserRepositoryTest.php
├── Integration/
│   ├── Database/
│   │   └── UserRepositoryIntegrationTest.php
│   └── Http/
│       └── ExternalApiGatewayTest.php
└── Contract/
    └── PaymentProcessorContractTest.php

Viết test method theo mẫu AAA + tên hàm tường minh

Mỗi test nên tuân thủ mô hình Arrange–Act–Assert, kết hợp với tên phương thức dạng givenWhenThen:

public function testGivenValidUserDataWhenCreatingUserThenUserIsPersistedAndReturnsId(): void
{
    // Arrange
    $userData = new UserData('alice@example.com', 'Alice', 'password123');
    $repository = $this->createMock(UserRepository::class);
    $repository->expects($this->once())
        ->method('save')
        ->willReturn(123);

    $service = new UserService($repository);

    // Act
    $result = $service->register($userData);

    // Assert
    $this->assertSame(123, $result->id());
    $this->assertInstanceOf(UserId::class, $result->id());
}

Sử dụng Data Provider một cách có chủ đích

Thay vì lặp lại logic kiểm thử, dùng provider để tách dữ liệu khỏi hành vi:

/**
 * @dataProvider validEmailProvider
 */
public function testEmailValidationAcceptsValidFormats(string $email): void
{
    $validator = new EmailValidator();
    $this->assertTrue($validator->isValid($email));
}

public function validEmailProvider(): array
{
    return [
        'standard' => ['user@domain.com'],
        'subdomain' => ['test@sub.example.org'],
        'unicode' => ['tést@exámple.δοκιμή'],
    ];
}

Xử lý mock một cách tiết chế

Theo nguyên tắc Clean Code, mock chỉ nên dùng khi cần kiểm soát đầu vào/đầu ra của dependency — không nên mock quá sâu hoặc giả lập toàn bộ hệ thống:

// ✅ Tốt: Mock interface cụ thể, xác định rõ hành vi
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->method('request')
    ->with('GET', 'https://api.example.com/users/1')
    ->willReturn(new Response(200, [], '{"id":1,"name":"John"}'));

// ❌ Tránh: Mock class trừu tượng hoặc mock quá nhiều method không liên quan
$mock = $this->getMockBuilder(SomeService::class)->setMethods(['a','b','c','d','e'])->getMock();

Tối ưu CI/CD pipeline cho kiểm thử

Trong GitHub Actions, tách các giai đoạn kiểm thử để tăng tốc độ phản hồi:

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
      - run: composer install --no-interaction --optimize-autoloader
      - run: vendor/bin/phpunit --testsuite Unit --coverage=none

  coverage-report:
    needs: unit-tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
      - run: composer install --no-interaction
      - run: vendor/bin/phpunit --coverage-html build/coverage
      - uses: codecov/codecov-action@v3

Thẻ: PHPUnit php cleancode testing TDD

Đăng vào ngày 29 tháng 9 lúc 18:57