2045c856e9
- Add 10 core testing guides (setup, AAA principles, PHPUnit/Pest, mocking, anti-patterns, PHP 8.4+, debugging, data factories, advanced edge cases) - Add 13 component testing guides covering all Laravel layers - Add standard templates for PHPUnit 10/11 and Pest v2/v3 including ArchitectureTest - Add Docker smart test runner, dependency installer, and CI/CD templates (GitHub Actions, GitLab CI) - Add 4 Claude Code skills (laravel-test-setup, laravel-unit-test with --all mode, laravel-test-review, laravel-mock-helper) - Add single unified installer script (sg-install-unit-testing.sh) with global support, CLI runners, and 1-click updater (sg-test-update) - Add comprehensive documentation (README.md, USAGE-GUIDE.md) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.0 KiB
7.0 KiB
Component Guide: Services & Action Classes
Services và Action classes là trái tim của logic nghiệp vụ trong các ứng dụng Laravel hiện đại. Đây là đối tượng quan trọng nhất cần đạt độ phủ Unit Test cao.
1. Mục Tiêu & Bản Chất Kiểm Thử
- Cần test:
- Xử lý logic nghiệp vụ, tính toán, điều kiện rẽ nhánh (
if/else,switch/match). - Đảm bảo các dependency (Repository, Payment Gateway, Mailer, Logger) được gọi đúng phương thức với đúng tham số.
- Các ngoại lệ (Exception) được ném ra đúng khi điều kiện tiên quyết không thỏa mãn.
- Giá trị trả về (Return value: DTO, Array, Model, Boolean) khớp với kỳ vọng.
- Xử lý logic nghiệp vụ, tính toán, điều kiện rẽ nhánh (
- Không cần test:
- Code của framework Laravel (ví dụ: không cần test xem hàm
collect()có hoạt động hay không). - Kết nối mạng thật hoặc truy vấn database thật (phải Mock hoàn toàn).
- Code của framework Laravel (ví dụ: không cần test xem hàm
2. Chiến Lược Giả Lập (Mocking Strategy)
- Dependencies trong Constructor: Sử dụng
Mockery::mock(Interface::class). - Static Facades: Sử dụng
Event::fake(),Queue::fake(),Log::shouldReceive(). - DTOs / Input Data: Khởi tạo DTO thật hoặc mảng thật.
3. Mã Mẫu Chuẩn: Service Class
Class Nguồn Cần Test: App\Services\PaymentService
namespace App\Services;
use App\Contracts\PaymentGatewayInterface;
use App\DTOs\PaymentData;
use App\Exceptions\PaymentFailedException;
class PaymentService
{
public function __construct(
private readonly PaymentGatewayInterface $gateway
) {}
public function process(PaymentData $data): string
{
if ($data->amount <= 0) {
throw new \InvalidArgumentException('Số tiền thanh toán phải lớn hơn 0');
}
$result = $this->gateway->charge($data->amount, $data->currency);
if (!$result->isSuccess()) {
throw new PaymentFailedException($result->getErrorMessage());
}
return $result->getTransactionId();
}
}
Test Chuẩn: PHPUnit 10+ (Attribute Style)
namespace Tests\Unit\Services;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
use Mockery;
use Mockery\MockInterface;
use App\Services\PaymentService;
use App\Contracts\PaymentGatewayInterface;
use App\Contracts\PaymentGatewayResultInterface;
use App\DTOs\PaymentData;
use App\Exceptions\PaymentFailedException;
class PaymentServiceTest extends TestCase
{
private PaymentGatewayInterface&MockInterface $gatewayMock;
private PaymentService $service;
protected function setUp(): void
{
parent::setUp();
$this->gatewayMock = Mockery::mock(PaymentGatewayInterface::class);
$this->service = new PaymentService($this->gatewayMock);
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
#[Test]
public function it_processes_payment_successfully_and_returns_transaction_id(): void
{
// Arrange
$paymentData = new PaymentData(amount: 150_000, currency: 'VND');
$resultMock = Mockery::mock(PaymentGatewayResultInterface::class);
$resultMock->shouldReceive('isSuccess')->once()->andReturnTrue();
$resultMock->shouldReceive('getTransactionId')->once()->andReturn('TXN_9999');
$this->gatewayMock->shouldReceive('charge')
->once()
->with(150_000, 'VND')
->andReturn($resultMock);
// Act
$transactionId = $this->service->process($paymentData);
// Assert
$this->assertSame('TXN_9999', $transactionId);
}
#[Test]
public function it_throws_invalid_argument_exception_when_amount_is_zero_or_negative(): void
{
// Arrange
$paymentData = new PaymentData(amount: 0, currency: 'VND');
// Assert Expectation
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Số tiền thanh toán phải lớn hơn 0');
// Act
$this->service->process($paymentData);
}
#[Test]
public function it_throws_payment_failed_exception_when_gateway_fails(): void
{
// Arrange
$paymentData = new PaymentData(amount: 50_000, currency: 'VND');
$resultMock = Mockery::mock(PaymentGatewayResultInterface::class);
$resultMock->shouldReceive('isSuccess')->once()->andReturnFalse();
$resultMock->shouldReceive('getErrorMessage')->once()->andReturn('Tài khoản không đủ số dư');
$this->gatewayMock->shouldReceive('charge')
->once()
->with(50_000, 'VND')
->andReturn($resultMock);
// Assert Expectation
$this->expectException(PaymentFailedException::class);
$this->expectExceptionMessage('Tài khoản không đủ số dư');
// Act
$this->service->process($paymentData);
}
}
Test Chuẩn: Pest PHP
use App\Services\PaymentService;
use App\Contracts\PaymentGatewayInterface;
use App\Contracts\PaymentGatewayResultInterface;
use App\DTOs\PaymentData;
use App\Exceptions\PaymentFailedException;
beforeEach(function () {
$this->gatewayMock = Mockery::mock(PaymentGatewayInterface::class);
$this->service = new PaymentService($this->gatewayMock);
});
describe('PaymentService', function () {
it('processes payment successfully and returns transaction id', function () {
$paymentData = new PaymentData(amount: 150_000, currency: 'VND');
$resultMock = Mockery::mock(PaymentGatewayResultInterface::class);
$resultMock->shouldReceive('isSuccess')->once()->andReturnTrue();
$resultMock->shouldReceive('getTransactionId')->once()->andReturn('TXN_9999');
$this->gatewayMock->shouldReceive('charge')
->once()
->with(150_000, 'VND')
->andReturn($resultMock);
$transactionId = $this->service->process($paymentData);
expect($transactionId)->toBe('TXN_9999');
});
it('throws exception when amount is zero or negative', function () {
$paymentData = new PaymentData(amount: -1000, currency: 'VND');
expect(fn () => $this->service->process($paymentData))
->toThrow(\InvalidArgumentException::class, 'Số tiền thanh toán phải lớn hơn 0');
});
it('throws payment failed exception when gateway fails', function () {
$paymentData = new PaymentData(amount: 50_000, currency: 'VND');
$resultMock = Mockery::mock(PaymentGatewayResultInterface::class);
$resultMock->shouldReceive('isSuccess')->once()->andReturnFalse();
$resultMock->shouldReceive('getErrorMessage')->once()->andReturn('Tài khoản không đủ số dư');
$this->gatewayMock->shouldReceive('charge')
->once()
->andReturn($resultMock);
expect(fn () => $this->service->process($paymentData))
->toThrow(PaymentFailedException::class, 'Tài khoản không đủ số dư');
});
});