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

176 lines
5.7 KiB
Markdown

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