Files
agent-unit-testing/docs/components/02-controllers.md
T
Pham-Tan-Thien 2045c856e9 feat: complete Smartgift AI Laravel unit testing toolkit
- 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>
2026-09-07 16:41:37 +07:00

5.7 KiB

Component Guide: Controllers (API & Web)

Controllers đóng vai trò điều hướng yêu cầu (HTTP Request) đến các tầng nghiệp vụ (Service/Action) và trả về phản hồi (Response: JSON, View, Redirect).


1. Ranh Giới: Unit Test vs Feature Test Cho Controller

  • Khuyến nghị ngành (Best Practice): 80% Controller nên được kiểm thử bằng Feature Test (sử dụng $this->getJson(), $this->postJson()) vì Controller gắn chặt với Router, Middleware và Validation Pipeline.
  • Khi nào viết Unit Test cho Controller?:
    • Khi bạn muốn kiểm thử riêng biệt logic điều hướng của Controller method mà không cần boot router, middleware hay database.
    • Kiểm tra xem Controller có nhận request và gọi đúng Service/Action với đúng tham số hay không.
    • Kiểm tra xem Controller có trả về đúng mã HTTP Status và cấu trúc JSON hay không.

2. Chiến Lược Unit Test Cho Controller

Để Unit Test một Controller một cách cô lập:

  1. Khởi tạo Controller trực tiếp với các Service/Action Mock trong constructor.
  2. Khởi tạo Request giả lập bằng Illuminate\Http\Request::create('/path', 'POST', $data) thay vì dispatch qua HTTP Kernel.
  3. Thực thi trực tiếp Controller Method: $response = $controller->store($request).
  4. Assert trên Response object: $response->getStatusCode(), $response->getData().

3. Mã Mẫu Chuẩn: Controller

Controller Nguồn: App\Http\Controllers\Api\UserController

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class UserController extends Controller
{
    public function __construct(
        private readonly UserService $userService
    ) {}

    public function show(int $id): JsonResponse
    {
        $user = $this->userService->findUserById($id);

        if (!$user) {
            return response()->json([
                'status'  => 'error',
                'message' => 'User not found',
            ], Response::HTTP_NOT_FOUND);
        }

        return response()->json([
            'status' => 'success',
            'data'   => $user,
        ], Response::HTTP_OK);
    }
}

Test Chuẩn: PHPUnit 10+

namespace Tests\Unit\Http\Controllers\Api;

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
use Mockery;
use Mockery\MockInterface;
use App\Http\Controllers\Api\UserController;
use App\Services\UserService;
use Symfony\Component\HttpFoundation\Response;

class UserControllerTest extends TestCase
{
    private UserService&MockInterface $userServiceMock;
    private UserController $controller;

    protected function setUp(): void
    {
        parent::setUp();
        $this->userServiceMock = Mockery::mock(UserService::class);
        $this->controller = new UserController($this->userServiceMock);
    }

    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }

    #[Test]
    public function it_returns_user_data_when_user_exists(): void
    {
        // Arrange
        $userData = ['id' => 1, 'name' => 'Thien PT', 'email' => 'thien@example.com'];
        $this->userServiceMock->shouldReceive('findUserById')
            ->once()
            ->with(1)
            ->andReturn($userData);

        // Act
        $response = $this->controller->show(1);

        // Assert
        $this->assertSame(Response::HTTP_OK, $response->getStatusCode());
        
        $responseData = json_decode($response->getContent(), true);
        $this->assertSame('success', $responseData['status']);
        $this->assertSame($userData, $responseData['data']);
    }

    #[Test]
    public function it_returns_404_not_found_when_user_does_not_exist(): void
    {
        // Arrange
        $this->userServiceMock->shouldReceive('findUserById')
            ->once()
            ->with(999)
            ->andReturnNull();

        // Act
        $response = $this->controller->show(999);

        // Assert
        $this->assertSame(Response::HTTP_NOT_FOUND, $response->getStatusCode());
        
        $responseData = json_decode($response->getContent(), true);
        $this->assertSame('error', $responseData['status']);
        $this->assertSame('User not found', $responseData['message']);
    }
}

Test Chuẩn: Pest PHP

use App\Http\Controllers\Api\UserController;
use App\Services\UserService;
use Symfony\Component\HttpFoundation\Response;

beforeEach(function () {
    $this->userServiceMock = Mockery::mock(UserService::class);
    $this->controller = new UserController($this->userServiceMock);
});

describe('UserController (Unit)', function () {
    it('returns user data with 200 OK when user exists', function () {
        $userData = ['id' => 1, 'name' => 'Thien PT'];
        $this->userServiceMock->shouldReceive('findUserById')
            ->once()
            ->with(1)
            ->andReturn($userData);

        $response = $this->controller->show(1);

        expect($response->getStatusCode())->toBe(Response::HTTP_OK);
        $data = json_decode($response->getContent(), true);
        expect($data['status'])->toBe('success');
        expect($data['data'])->toBe($userData);
    });

    it('returns 404 Not Found when user does not exist', function () {
        $this->userServiceMock->shouldReceive('findUserById')
            ->once()
            ->with(999)
            ->andReturnNull();

        $response = $this->controller->show(999);

        expect($response->getStatusCode())->toBe(Response::HTTP_NOT_FOUND);
    });
});