# 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` ```php 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+ ```php 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 ```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); }); }); ```