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>
176 lines
5.7 KiB
Markdown
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);
|
|
});
|
|
});
|
|
```
|