feat: add comprehensive unit testing support for CakePHP
- Add 4 CakePHP Claude Code skills: cakephp-unit-test, cakephp-test-setup, cakephp-test-review, cakephp-mock-helper - Add 11 CakePHP PHPUnit component templates (Table, Entity, Controller, Service, Command, Component, Middleware, Mailer, Form, Behavior, Helper) - Add CakePHP configs (phpunit.xml.dist, app_local.test.php for SQLite in-memory) - Add comprehensive CakePHP documentation and component guides - Update test-runner.sh and installer to auto-detect Laravel and CakePHP - Add CakePHP CI/CD templates for GitHub Actions and GitLab CI - Update system prompt, verification checklist, and README with dedicated guides
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# 🍰 CakePHP Unit & Integration Testing Guide
|
||||
|
||||
> **Cẩm nang toàn diện về quy chuẩn, kiến trúc và phương pháp kiểm thử tự động cho dự án PHP CakePHP (3.x, 4.x, 5.x+, PHP 7.4 đến 8.4+).**
|
||||
|
||||
---
|
||||
|
||||
## 1. Triết Lý & Nguyên Tắc Cốt Lõi
|
||||
|
||||
1. **Mô hình AAA (Arrange - Act - Assert)**:
|
||||
- Mọi phương thức test bắt buộc phải có phân tách rõ ràng 3 bước:
|
||||
- `// Arrange`: Chuẩn bị payload, cấu hình session/auth, mock dependency.
|
||||
- `// Act`: Thực thi phương thức (hoặc gọi HTTP endpoint, hoặc gọi console command).
|
||||
- `// Assert`: So sánh kết quả, kiểm tra HTTP code, kiểm tra dữ liệu thay đổi trong DB.
|
||||
|
||||
2. **Bảo Vệ Database Tuyệt Đối (Zero Leakage)**:
|
||||
- Mọi test tương tác cơ sở dữ liệu **PHẢI sử dụng kết nối `test`** (tốt nhất là SQLite in-memory `:memory:`).
|
||||
- **Tuyệt đối không chạy test trên kết nối `default`** (tránh xóa nhầm hoặc làm bẩn dữ liệu dev/local).
|
||||
- Sử dụng cơ chế Transaction Strategy hoặc Fixture Strategy của CakePHP để rollback dữ liệu tự động sau mỗi test.
|
||||
|
||||
3. **Phân Biệt Rõ Mocking vs Fixtures**:
|
||||
- **Dùng Fixtures**: Khi kiểm thử Table, Custom Finder, ORM query phức tạp.
|
||||
- **Dùng Mocks**: Khi gọi Web service bên ngoài (Stripe, Twilio, AWS S3, External API) hoặc các Service Interface trừu tượng.
|
||||
- **Không Mock**: Entity (dùng Entity thật), Array, DTOs, Value Objects.
|
||||
|
||||
---
|
||||
|
||||
## 2. Các Trait Kiểm Thử Quan Trọng của CakePHP
|
||||
|
||||
CakePHP cung cấp sẵn các Trait vô cùng mạnh mẽ trong `Cake\TestSuite`:
|
||||
|
||||
### 1. `Cake\TestSuite\IntegrationTestTrait` (Dành cho Controllers / HTTP Endpoints)
|
||||
Cho phép gửi request HTTP ảo vào ứng dụng CakePHP mà không cần mở web server thật:
|
||||
```php
|
||||
use Cake\TestSuite\IntegrationTestTrait;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
class ArticlesControllerTest extends TestCase
|
||||
{
|
||||
use IntegrationTestTrait;
|
||||
|
||||
public function testIndex(): void
|
||||
{
|
||||
$this->get('/articles');
|
||||
$this->assertResponseOk();
|
||||
$this->assertResponseContains('Latest Articles');
|
||||
}
|
||||
|
||||
public function testAddPostWithAuthAndCsrf(): void
|
||||
{
|
||||
// Giả lập đăng nhập
|
||||
$this->session(['Auth.User.id' => 1, 'Auth.User.role' => 'admin']);
|
||||
|
||||
// Bật CSRF & Security token
|
||||
$this->enableCsrfToken();
|
||||
$this->enableSecurityToken();
|
||||
|
||||
$this->post('/articles/add', [
|
||||
'title' => 'CakePHP Testing Pro',
|
||||
'body' => 'High quality tests.',
|
||||
]);
|
||||
|
||||
$this->assertResponseSuccess();
|
||||
$this->assertRedirect(['action' => 'index']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `Cake\Console\TestSuite\ConsoleIntegrationTestTrait` (Dành cho Commands / Shells)
|
||||
Giúp test CLI Command một cách trực quan:
|
||||
```php
|
||||
use Cake\Console\TestSuite\ConsoleIntegrationTestTrait;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
class SendReportCommandTest extends TestCase
|
||||
{
|
||||
use ConsoleIntegrationTestTrait;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->useCommandRunner();
|
||||
}
|
||||
|
||||
public function testExecuteSuccess(): void
|
||||
{
|
||||
$this->exec('send_report --daily');
|
||||
$this->assertExitSuccess();
|
||||
$this->assertOutputContains('Reports sent successfully');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. `Cake\TestSuite\EmailTrait` (Dành cho Mailers)
|
||||
Kiểm tra email được gửi đi mà không thực sự gửi qua SMTP:
|
||||
```php
|
||||
use Cake\TestSuite\EmailTrait;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
class UserMailerTest extends TestCase
|
||||
{
|
||||
use EmailTrait;
|
||||
|
||||
public function testWelcomeEmail(): void
|
||||
{
|
||||
$mailer = new UserMailer();
|
||||
$mailer->send('welcome', ['user@example.com', 'Alex']);
|
||||
|
||||
$this->assertMailSentTo('user@example.com');
|
||||
$this->assertMailContainsText('Welcome Alex');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Quản Lý Test Database & Fixtures
|
||||
|
||||
### Cấu hình SQLite In-Memory trong `config/app_local.php`:
|
||||
```php
|
||||
'Datasources' => [
|
||||
'test' => [
|
||||
'className' => 'Cake\Database\Connection',
|
||||
'driver' => 'Cake\Database\Driver\Sqlite',
|
||||
'database' => ':memory:',
|
||||
'encoding' => 'utf8',
|
||||
'timezone' => 'UTC',
|
||||
'cacheMetadata' => true,
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
### Khai báo Fixtures trong Test Case:
|
||||
```php
|
||||
protected array $fixtures = [
|
||||
'app.Users',
|
||||
'app.Articles',
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Cách Chạy Test Tối Ưu
|
||||
|
||||
1. **Chạy qua Smart Test Runner**:
|
||||
```bash
|
||||
./test-runner.sh
|
||||
# Chạy 1 file test cụ thể:
|
||||
./test-runner.sh --filter=ArticlesControllerTest
|
||||
```
|
||||
2. **Chạy qua CakePHP CLI**:
|
||||
```bash
|
||||
bin/cake test app tests/TestCase/Model/Table/ArticlesTableTest.php
|
||||
```
|
||||
3. **Chạy qua PHPUnit trực tiếp**:
|
||||
```bash
|
||||
vendor/bin/phpunit --filter=ArticlesTableTest
|
||||
```
|
||||
4. **Đo Code Coverage cho SonarQube**:
|
||||
```bash
|
||||
./test-runner.sh --coverage-clover coverage.xml
|
||||
```
|
||||
@@ -0,0 +1,211 @@
|
||||
# 🧩 CakePHP Components Unit Testing Reference
|
||||
|
||||
> **Hướng dẫn chi tiết cách viết test cho từng tầng thành phần trong CakePHP (Table, Entity, Controller, Service, Command, Component, Middleware, Mailer, Form, Behavior).**
|
||||
|
||||
---
|
||||
|
||||
## 1. Table Classes (`src/Model/Table/*Table.php`)
|
||||
- **Base Class**: `Cake\TestSuite\TestCase`
|
||||
- **Các điểm cần test**:
|
||||
- `validationDefault()`: Đầu vào hợp lệ, thiếu trường bắt buộc, sai format (email, regex, số âm).
|
||||
- `buildRules()`: Quy tắc toàn vẹn dữ liệu (`isUnique`, `existsIn`).
|
||||
- Custom Finders (`findPublished()`, `findAuth()`): Truy vấn trả về đúng bản ghi mong muốn.
|
||||
- Custom methods: Xử lý nghiệp vụ lưu dữ liệu.
|
||||
|
||||
```php
|
||||
namespace App\Test\TestCase\Model\Table;
|
||||
|
||||
use App\Model\Table\ArticlesTable;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
class ArticlesTableTest extends TestCase
|
||||
{
|
||||
protected ArticlesTable $Articles;
|
||||
protected array $fixtures = ['app.Articles'];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->Articles = $this->getTableLocator()->get('Articles');
|
||||
}
|
||||
|
||||
public function testFindPublished(): void
|
||||
{
|
||||
$query = $this->Articles->find('published');
|
||||
$this->assertInstanceOf(\Cake\ORM\Query::class, $query);
|
||||
$result = $query->toArray();
|
||||
$this->assertNotEmpty($result);
|
||||
foreach ($result as $article) {
|
||||
$this->assertTrue($article->published);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Entity Classes (`src/Model/Entity/*.php`)
|
||||
- **Base Class**: `Cake\TestSuite\TestCase`
|
||||
- **Các điểm cần test**:
|
||||
- `_setPassword()` (Hashing logic).
|
||||
- Virtual Fields (`_getFullName()`).
|
||||
- `$_accessible` properties (Bảo vệ Mass Assignment).
|
||||
|
||||
```php
|
||||
namespace App\Test\TestCase\Model\Entity;
|
||||
|
||||
use App\Model\Entity\User;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
class UserTest extends TestCase
|
||||
{
|
||||
public function testPasswordIsHashed(): void
|
||||
{
|
||||
$user = new User(['password' => 'secret123']);
|
||||
$this->assertNotEquals('secret123', $user->password);
|
||||
$this->assertTrue((new \Authentication\PasswordHasher\DefaultPasswordHasher())->check('secret123', $user->password));
|
||||
}
|
||||
|
||||
public function testVirtualFieldFullName(): void
|
||||
{
|
||||
$user = new User(['first_name' => 'John', 'last_name' => 'Doe']);
|
||||
$this->assertSame('John Doe', $user->full_name);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Controller Classes (`src/Controller/*Controller.php`)
|
||||
- **Base Class**: `Cake\TestSuite\TestCase` với `Cake\TestSuite\IntegrationTestTrait`
|
||||
- **Các điểm cần test**:
|
||||
- HTTP Status: `200` OK, `302` Redirect, `403` Forbidden, `404` Not Found, `422` Unprocessable Entity.
|
||||
- Flash messages: `$this->assertFlashMessage()`.
|
||||
- Session & Auth: Giả lập đăng nhập qua `$this->session()`.
|
||||
- CSRF & Security Tokens: Bắt buộc dùng `$this->enableCsrfToken()` và `$this->enableSecurityToken()` khi test POST/PUT/DELETE.
|
||||
|
||||
```php
|
||||
namespace App\Test\TestCase\Controller;
|
||||
|
||||
use Cake\TestSuite\IntegrationTestTrait;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
class ArticlesControllerTest extends TestCase
|
||||
{
|
||||
use IntegrationTestTrait;
|
||||
protected array $fixtures = ['app.Articles', 'app.Users'];
|
||||
|
||||
public function testAddWithoutAuthRedirectsToLogin(): void
|
||||
{
|
||||
$this->get('/articles/add');
|
||||
$this->assertRedirectContains('/login');
|
||||
}
|
||||
|
||||
public function testAddSuccess(): void
|
||||
{
|
||||
$this->session(['Auth.User.id' => 1]);
|
||||
$this->enableCsrfToken();
|
||||
$this->enableSecurityToken();
|
||||
|
||||
$this->post('/articles/add', [
|
||||
'title' => 'Sample Article',
|
||||
'body' => 'Body text',
|
||||
'published' => true,
|
||||
]);
|
||||
|
||||
$this->assertRedirect(['action' => 'index']);
|
||||
$this->assertFlashMessage('The article has been saved.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Service Classes (`src/Service/*.php`)
|
||||
- **Base Class**: `Cake\TestSuite\TestCase`
|
||||
- **Các điểm cần test**:
|
||||
- Các dependency được mock bằng PHPUnit Mock hoặc Mockery.
|
||||
- Kiểm thử toàn bộ các nhánh rẽ điều kiện (Success, Invalid Data, 3rd-Party Failure).
|
||||
|
||||
```php
|
||||
namespace App\Test\TestCase\Service;
|
||||
|
||||
use App\Service\PaymentProcessingService;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
class PaymentProcessingServiceTest extends TestCase
|
||||
{
|
||||
public function testChargeSuccess(): void
|
||||
{
|
||||
$clientMock = $this->createMock(\App\External\StripeClient::class);
|
||||
$clientMock->expects($this->once())
|
||||
->method('createCharge')
|
||||
->willReturn(['id' => 'ch_123', 'status' => 'succeeded']);
|
||||
|
||||
$service = new PaymentProcessingService($clientMock);
|
||||
$result = $service->processOrder(100.0, 'tok_visa');
|
||||
|
||||
$this->assertTrue($result->isSuccess());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Console Command Classes (`src/Command/*Command.php`)
|
||||
- **Base Class**: `Cake\TestSuite\TestCase` với `Cake\Console\TestSuite\ConsoleIntegrationTestTrait`
|
||||
- **Các điểm cần test**:
|
||||
- `assertExitSuccess()` và `assertExitError()`.
|
||||
- `assertOutputContains()`.
|
||||
- Arguments và Options flags.
|
||||
|
||||
```php
|
||||
namespace App\Test\TestCase\Command;
|
||||
|
||||
use Cake\Console\TestSuite\ConsoleIntegrationTestTrait;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
class SyncInventoryCommandTest extends TestCase
|
||||
{
|
||||
use ConsoleIntegrationTestTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->useCommandRunner();
|
||||
}
|
||||
|
||||
public function testSyncAll(): void
|
||||
{
|
||||
$this->exec('sync_inventory --all');
|
||||
$this->assertExitSuccess();
|
||||
$this->assertOutputContains('Inventory sync completed.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Controller Component Classes (`src/Controller/Component/*.php`)
|
||||
- Test logic tái sử dụng gắn vào Controller, dùng `ComponentRegistry`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Middleware Classes (`src/Middleware/*.php`)
|
||||
- Test PSR-15 Middleware với `Cake\Http\ServerRequest` và Response Handler.
|
||||
|
||||
---
|
||||
|
||||
## 8. Mailer Classes (`src/Mailer/*Mailer.php`)
|
||||
- Sử dụng `Cake\TestSuite\EmailTrait`, kiểm tra email recipient, subject, và template body.
|
||||
|
||||
---
|
||||
|
||||
## 9. Form Classes (`src/Form/*Form.php`)
|
||||
- Test standalone forms không gắn với ORM table (ContactForm, SearchFilterForm).
|
||||
- Kiểm tra `validationDefault()` và `_execute()`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Behavior Classes (`src/Model/Behavior/*.php`)
|
||||
- Test ORM Behavior hooks (`beforeSave`, `beforeFind`, `afterSave`).
|
||||
Reference in New Issue
Block a user