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:
2026-09-14 10:19:40 +07:00
parent 0dce2ef812
commit 7425b263d5
28 changed files with 2210 additions and 356 deletions
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Model\Behavior;
use App\Model\Behavior\{{BEHAVIOR_NAME}}Behavior;
use Cake\ORM\Table;
use Cake\TestSuite\TestCase;
/**
* App\Model\Behavior\{{BEHAVIOR_NAME}}Behavior Test Case
*/
class {{BEHAVIOR_NAME}}BehaviorTest extends TestCase
{
/**
* Test subject
*
* @var \App\Model\Behavior\{{BEHAVIOR_NAME}}Behavior
*/
protected {{BEHAVIOR_NAME}}Behavior ${{VARIABLE_NAME}};
/**
* Table instance
*
* @var \Cake\ORM\Table
*/
protected Table $table;
/**
* setUp method
*/
protected function setUp(): void
{
parent::setUp();
$this->table = new Table(['table' => 'test_table']);
$this->{{VARIABLE_NAME}} = new {{BEHAVIOR_NAME}}Behavior($this->table);
}
/**
* tearDown method
*/
protected function tearDown(): void
{
unset($this->{{VARIABLE_NAME}}, $this->table);
parent::tearDown();
}
/**
* Test behavior initialization
*/
public function testInitialization(): void
{
$this->assertInstanceOf({{BEHAVIOR_NAME}}Behavior::class, $this->{{VARIABLE_NAME}});
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Command;
use Cake\Console\TestSuite\ConsoleIntegrationTestTrait;
use Cake\TestSuite\TestCase;
/**
* App\Command\{{COMMAND_NAME}}Command Test Case
*/
class {{COMMAND_NAME}}CommandTest extends TestCase
{
use ConsoleIntegrationTestTrait;
/**
* setUp method
*/
protected function setUp(): void
{
parent::setUp();
$this->useCommandRunner();
}
/**
* Test execute with success
*/
public function testExecuteSuccess(): void
{
// Act
$this->exec('{{COMMAND_SLUG}}');
// Assert
$this->assertExitSuccess();
$this->assertOutputContains('Done');
}
/**
* Test execute with invalid argument
*/
public function testExecuteFailsOnMissingArgument(): void
{
// Act
$this->exec('{{COMMAND_SLUG}} --invalid-flag');
// Assert
$this->assertExitError();
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Controller\Component;
use App\Controller\Component\{{COMPONENT_NAME}}Component;
use Cake\Controller\ComponentRegistry;
use Cake\Controller\Controller;
use Cake\Http\ServerRequest;
use Cake\TestSuite\TestCase;
/**
* App\Controller\Component\{{COMPONENT_NAME}}Component Test Case
*/
class {{COMPONENT_NAME}}ComponentTest extends TestCase
{
/**
* Test subject
*
* @var \App\Controller\Component\{{COMPONENT_NAME}}Component
*/
protected {{COMPONENT_NAME}}Component ${{VARIABLE_NAME}};
/**
* setUp method
*/
protected function setUp(): void
{
parent::setUp();
$controller = new Controller(new ServerRequest());
$registry = new ComponentRegistry($controller);
$this->{{VARIABLE_NAME}} = new {{COMPONENT_NAME}}Component($registry);
}
/**
* tearDown method
*/
protected function tearDown(): void
{
unset($this->{{VARIABLE_NAME}});
parent::tearDown();
}
/**
* Test basic method
*/
public function testDoActionSuccess(): void
{
// Act
$result = $this->{{VARIABLE_NAME}}->doAction();
// Assert
$this->assertTrue($result);
}
}
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Controller;
use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;
/**
* App\Controller\{{CONTROLLER_NAME}}Controller Test Case
*
* @uses \App\Controller\{{CONTROLLER_NAME}}Controller
*/
class {{CONTROLLER_NAME}}ControllerTest extends TestCase
{
use IntegrationTestTrait;
/**
* Fixtures
*
* @var list<string>
*/
protected array $fixtures = [
'app.{{MODEL_NAME}}',
];
/**
* Test index method
*/
public function testIndex(): void
{
// Act
$this->get('/{{ROUTE_PATH}}');
// Assert
$this->assertResponseOk();
$this->assertResponseContains('{{EXPECTED_TEXT}}');
}
/**
* Test view method with valid ID
*/
public function testViewValidId(): void
{
// Act
$this->get('/{{ROUTE_PATH}}/view/1');
// Assert
$this->assertResponseOk();
}
/**
* Test add method GET renders form
*/
public function testAddGet(): void
{
// Act
$this->get('/{{ROUTE_PATH}}/add');
// Assert
$this->assertResponseOk();
}
/**
* Test add method POST with valid data
*/
public function testAddPostSuccess(): void
{
// Arrange
$this->enableCsrfToken();
$this->enableSecurityToken();
$data = [
'name' => 'New Item',
'status' => 'active',
];
// Act
$this->post('/{{ROUTE_PATH}}/add', $data);
// Assert
$this->assertResponseSuccess();
$this->assertRedirect(['action' => 'index']);
$this->assertFlashMessage('The {{ENTITY_LOWER}} has been saved.');
}
/**
* Test add method POST with validation errors
*/
public function testAddPostValidationFailure(): void
{
// Arrange
$this->enableCsrfToken();
$this->enableSecurityToken();
$data = []; // Empty or invalid data
// Act
$this->post('/{{ROUTE_PATH}}/add', $data);
// Assert
$this->assertResponseOk(); // Renders form view with errors
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Model\Entity;
use App\Model\Entity\{{ENTITY_NAME}};
use Cake\TestSuite\TestCase;
/**
* App\Model\Entity\{{ENTITY_NAME}} Test Case
*/
class {{ENTITY_NAME}}Test extends TestCase
{
/**
* Test subject
*
* @var \App\Model\Entity\{{ENTITY_NAME}}
*/
protected {{ENTITY_NAME}} ${{VARIABLE_NAME}};
/**
* setUp method
*/
protected function setUp(): void
{
parent::setUp();
$this->{{VARIABLE_NAME}} = new {{ENTITY_NAME}}();
}
/**
* tearDown method
*/
protected function tearDown(): void
{
unset($this->{{VARIABLE_NAME}});
parent::tearDown();
}
/**
* Test accessible fields configuration
*/
public function testAccessibleFields(): void
{
// Arrange
$data = [
'name' => 'John Doe',
'email' => 'john@example.com',
'id' => 999, // Typically non-accessible or guarded
];
// Act
$this->{{VARIABLE_NAME}}->set($data);
// Assert
$this->assertSame('John Doe', $this->{{VARIABLE_NAME}}->name);
$this->assertSame('john@example.com', $this->{{VARIABLE_NAME}}->email);
}
/**
* Test virtual fields if present
*/
public function testVirtualProperties(): void
{
// Arrange
$this->{{VARIABLE_NAME}}->set([
'first_name' => 'John',
'last_name' => 'Doe',
]);
// Act & Assert
if ($this->{{VARIABLE_NAME}}->has('full_name')) {
$this->assertSame('John Doe', $this->{{VARIABLE_NAME}}->full_name);
} else {
$this->assertTrue(true);
}
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Form;
use App\Form\{{FORM_NAME}}Form;
use Cake\TestSuite\TestCase;
/**
* App\Form\{{FORM_NAME}}Form Test Case
*/
class {{FORM_NAME}}FormTest extends TestCase
{
/**
* Test subject
*
* @var \App\Form\{{FORM_NAME}}Form
*/
protected {{FORM_NAME}}Form ${{VARIABLE_NAME}};
/**
* setUp method
*/
protected function setUp(): void
{
parent::setUp();
$this->{{VARIABLE_NAME}} = new {{FORM_NAME}}Form();
}
/**
* tearDown method
*/
protected function tearDown(): void
{
unset($this->{{VARIABLE_NAME}});
parent::tearDown();
}
/**
* Test validationDefault with valid data
*/
public function testValidationSuccess(): void
{
// Arrange
$data = [
'email' => 'user@example.com',
'message' => 'Hello World',
];
// Act
$isValid = $this->{{VARIABLE_NAME}}->validate($data);
// Assert
$this->assertTrue($isValid);
$this->assertEmpty($this->{{VARIABLE_NAME}}->getErrors());
}
/**
* Test execute method
*/
public function testExecuteSuccess(): void
{
// Arrange
$data = [
'email' => 'user@example.com',
'message' => 'Hello World',
];
// Act
$result = $this->{{VARIABLE_NAME}}->execute($data);
// Assert
$this->assertTrue($result);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\View\Helper;
use App\View\Helper\{{HELPER_NAME}}Helper;
use Cake\TestSuite\TestCase;
use Cake\View\View;
/**
* App\View\Helper\{{HELPER_NAME}}Helper Test Case
*/
class {{HELPER_NAME}}HelperTest extends TestCase
{
/**
* Test subject
*
* @var \App\View\Helper\{{HELPER_NAME}}Helper
*/
protected {{HELPER_NAME}}Helper ${{VARIABLE_NAME}};
/**
* setUp method
*/
protected function setUp(): void
{
parent::setUp();
$view = new View();
$this->{{VARIABLE_NAME}} = new {{HELPER_NAME}}Helper($view);
}
/**
* tearDown method
*/
protected function tearDown(): void
{
unset($this->{{VARIABLE_NAME}});
parent::tearDown();
}
/**
* Test helper initialization and rendering
*/
public function testInitialization(): void
{
$this->assertInstanceOf({{HELPER_NAME}}Helper::class, $this->{{VARIABLE_NAME}});
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Mailer;
use App\Mailer\{{MAILER_NAME}}Mailer;
use Cake\TestSuite\EmailTrait;
use Cake\TestSuite\TestCase;
/**
* App\Mailer\{{MAILER_NAME}}Mailer Test Case
*/
class {{MAILER_NAME}}MailerTest extends TestCase
{
use EmailTrait;
/**
* Test subject
*
* @var \App\Mailer\{{MAILER_NAME}}Mailer
*/
protected {{MAILER_NAME}}Mailer ${{VARIABLE_NAME}};
/**
* setUp method
*/
protected function setUp(): void
{
parent::setUp();
$this->{{VARIABLE_NAME}} = new {{MAILER_NAME}}Mailer();
}
/**
* Test send welcome email
*/
public function testSendWelcomeEmail(): void
{
// Act
$this->{{VARIABLE_NAME}}->send('welcome', ['user@example.com', 'John Doe']);
// Assert
$this->assertMailSentTo('user@example.com');
$this->assertMailContainsText('John Doe');
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Middleware;
use App\Middleware\{{MIDDLEWARE_NAME}}Middleware;
use Cake\Http\Response;
use Cake\Http\ServerRequest;
use Cake\TestSuite\TestCase;
use TestApp\Http\TestRequestHandler;
/**
* App\Middleware\{{MIDDLEWARE_NAME}}Middleware Test Case
*/
class {{MIDDLEWARE_NAME}}MiddlewareTest extends TestCase
{
/**
* Test process method
*/
public function testProcess(): void
{
// Arrange
$middleware = new {{MIDDLEWARE_NAME}}Middleware();
$request = new ServerRequest(['url' => '/test-path']);
$handler = new class implements \Psr\Http\Server\RequestHandlerInterface {
public function handle(\Psr\Http\Message\ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface
{
return new Response();
}
};
// Act
$response = $middleware->process($request, $handler);
// Assert
$this->assertInstanceOf(\Psr\Http\Message\ResponseInterface::class, $response);
$this->assertSame(200, $response->getStatusCode());
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Service;
use App\Service\{{SERVICE_NAME}}Service;
use Cake\TestSuite\TestCase;
/**
* App\Service\{{SERVICE_NAME}}Service Test Case
*/
class {{SERVICE_NAME}}ServiceTest extends TestCase
{
/**
* Test subject
*
* @var \App\Service\{{SERVICE_NAME}}Service
*/
protected {{SERVICE_NAME}}Service ${{VARIABLE_NAME}};
/**
* setUp method
*/
protected function setUp(): void
{
parent::setUp();
// Arrange dependencies
$this->{{VARIABLE_NAME}} = new {{SERVICE_NAME}}Service();
}
/**
* tearDown method
*/
protected function tearDown(): void
{
unset($this->{{VARIABLE_NAME}});
parent::tearDown();
}
/**
* Test execution success (Happy Path)
*/
public function testExecuteSuccess(): void
{
// Arrange
$input = 'sample_input';
// Act
$result = $this->{{VARIABLE_NAME}}->execute($input);
// Assert
$this->assertNotNull($result);
}
/**
* Test execution with invalid input (Exception Path)
*/
public function testExecuteThrowsExceptionOnInvalidInput(): void
{
// Assert
$this->expectException(\InvalidArgumentException::class);
// Act
$this->{{VARIABLE_NAME}}->execute('');
}
}
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Model\Table;
use App\Model\Table\{{TABLE_NAME}}Table;
use Cake\TestSuite\TestCase;
/**
* App\Model\Table\{{TABLE_NAME}}Table Test Case
*/
class {{TABLE_NAME}}TableTest extends TestCase
{
/**
* Test subject
*
* @var \App\Model\Table\{{TABLE_NAME}}Table
*/
protected {{TABLE_NAME}}Table ${{VARIABLE_NAME}};
/**
* Fixtures
*
* @var list<string>
*/
protected array $fixtures = [
'app.{{TABLE_NAME}}',
];
/**
* setUp method
*/
protected function setUp(): void
{
parent::setUp();
$config = $this->getTableLocator()->exists('{{TABLE_NAME}}') ? [] : ['className' => {{TABLE_NAME}}Table::class];
$this->{{VARIABLE_NAME}} = $this->getTableLocator()->get('{{TABLE_NAME}}', $config);
}
/**
* tearDown method
*/
protected function tearDown(): void
{
unset($this->{{VARIABLE_NAME}});
parent::tearDown();
}
/**
* Test validationDefault method with valid data
*/
public function testValidationDefaultSuccess(): void
{
// Arrange
$data = [
'name' => 'Valid Title',
'status' => 'active',
];
// Act
$entity = $this->{{VARIABLE_NAME}}->newEntity($data);
// Assert
$this->assertEmpty($entity->getErrors());
}
/**
* Test validationDefault method with missing required fields
*/
public function testValidationDefaultFailsOnRequiredFields(): void
{
// Arrange
$data = [];
// Act
$entity = $this->{{VARIABLE_NAME}}->newEntity($data);
// Assert
$this->assertNotEmpty($entity->getErrors());
$this->assertArrayHasKey('name', $entity->getErrors());
}
/**
* Test save and find
*/
public function testSaveAndFind(): void
{
// Arrange
$entity = $this->{{VARIABLE_NAME}}->newEntity([
'name' => 'Sample Record',
'status' => 'active',
]);
// Act
$result = $this->{{VARIABLE_NAME}}->save($entity);
// Assert
$this->assertNotFalse($result);
$this->assertNotNull($result->id);
$saved = $this->{{VARIABLE_NAME}}->get($result->id);
$this->assertSame('Sample Record', $saved->name);
}
}