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>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\{{Namespace}};
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Mockery;
|
||||
use {{TargetClassFullName}};
|
||||
|
||||
class {{ClassName}}Test extends TestCase
|
||||
{
|
||||
private {{ClassName}} $action;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// Khởi tạo action kèm mock dependencies
|
||||
// $this->action = new {{ClassName}}(...);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_handles_action_invocation_successfully(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
// $result = ($this->action)($dto);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
|
||||
class ArchitectureTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function no_debugging_statements_exist_in_application(): void
|
||||
{
|
||||
$appPath = realpath(__DIR__ . '/../../app');
|
||||
$this->assertDirectoryExists($appPath);
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($appPath, \FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
$forbiddenPatterns = ['/\bdd\(/', '/\bdump\(/', '/\bvar_dump\(/', '/\bprint_r\(/', '/\bray\(/'];
|
||||
$violations = [];
|
||||
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = file_get_contents($file->getRealPath());
|
||||
foreach ($forbiddenPatterns as $pattern) {
|
||||
if (preg_match($pattern, $content)) {
|
||||
$violations[] = $file->getRelativePathname() . " matches pattern {$pattern}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertEmpty(
|
||||
$violations,
|
||||
"Found forbidden debugging statements:\n" . implode("\n", $violations)
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function controllers_must_have_controller_suffix(): void
|
||||
{
|
||||
$controllersPath = realpath(__DIR__ . '/../../app/Http/Controllers');
|
||||
if (!$controllersPath || !is_dir($controllersPath)) {
|
||||
$this->markTestSkipped('Controllers directory does not exist.');
|
||||
}
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($controllersPath, \FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
$invalidFiles = [];
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = $file->getBasename('.php');
|
||||
if (!str_ends_with($filename, 'Controller')) {
|
||||
$invalidFiles[] = $file->getRelativePathname();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertEmpty(
|
||||
$invalidFiles,
|
||||
"Controllers must end with 'Controller':\n" . implode("\n", $invalidFiles)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\{{Namespace}};
|
||||
|
||||
use Tests\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Illuminate\Console\Command;
|
||||
use {{TargetClassFullName}};
|
||||
|
||||
class {{ClassName}}Test extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function it_executes_artisan_command_successfully(): void
|
||||
{
|
||||
$this->artisan('{{CommandSignature}}')
|
||||
->assertExitCode(Command::SUCCESS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\{{Namespace}};
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Mockery;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use {{TargetClassFullName}};
|
||||
|
||||
class {{ClassName}}Test extends TestCase
|
||||
{
|
||||
private {{ClassName}} $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// $this->controller = new {{ClassName}}(...);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_returns_successful_response(): void
|
||||
{
|
||||
// Arrange
|
||||
// $request = Request::create('/api/resource', 'GET');
|
||||
|
||||
// Act
|
||||
// $response = $this->controller->index($request);
|
||||
|
||||
// Assert
|
||||
// $this->assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\{{Namespace}};
|
||||
|
||||
use Tests\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use {{TargetClassFullName}};
|
||||
|
||||
class {{ClassName}}Test extends TestCase
|
||||
{
|
||||
private {{ClassName}} $request;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->request = new {{ClassName}}();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_authorizes_request_appropriately(): void
|
||||
{
|
||||
// Assert authorize() returns expected boolean
|
||||
$this->assertTrue($this->request->authorize());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_passes_validation_with_valid_payload(): void
|
||||
{
|
||||
// Arrange
|
||||
$validPayload = [
|
||||
// Cung cấp dữ liệu chuẩn
|
||||
];
|
||||
|
||||
// Act
|
||||
$validator = Validator::make($validPayload, $this->request->rules());
|
||||
|
||||
// Assert
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_fails_validation_when_required_fields_are_missing(): void
|
||||
{
|
||||
// Arrange
|
||||
$emptyPayload = [];
|
||||
|
||||
// Act
|
||||
$validator = Validator::make($emptyPayload, $this->request->rules());
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\{{Namespace}};
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Mockery;
|
||||
use {{TargetClassFullName}};
|
||||
|
||||
class {{ClassName}}Test extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_handles_job_execution_successfully(): void
|
||||
{
|
||||
// Arrange
|
||||
// $dependencyMock = Mockery::mock(Service::class);
|
||||
// $dependencyMock->shouldReceive('execute')->once();
|
||||
|
||||
// $job = new {{ClassName}}(...);
|
||||
|
||||
// Act
|
||||
// $job->handle($dependencyMock);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\{{Namespace}};
|
||||
|
||||
use Tests\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use {{TargetClassFullName}};
|
||||
|
||||
class {{ClassName}}Test extends TestCase
|
||||
{
|
||||
private {{ClassName}} $middleware;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->middleware = new {{ClassName}}();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_passes_request_to_next_closure_when_authorized(): void
|
||||
{
|
||||
// Arrange
|
||||
$request = Request::create('/test-route', 'GET');
|
||||
$nextCalled = false;
|
||||
|
||||
$next = function ($req) use (&$nextCalled) {
|
||||
$nextCalled = true;
|
||||
return response('OK', Response::HTTP_OK);
|
||||
};
|
||||
|
||||
// Act
|
||||
$response = $this->middleware->handle($request, $next);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($nextCalled);
|
||||
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\{{Namespace}};
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use {{TargetClassFullName}};
|
||||
use App\Models\User;
|
||||
|
||||
class {{ClassName}}Test extends TestCase
|
||||
{
|
||||
private {{ClassName}} $policy;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->policy = new {{ClassName}}();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_allows_authorized_user_to_perform_action(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = new User(['id' => 1]);
|
||||
|
||||
// Act & Assert
|
||||
// $this->assertTrue($this->policy->view($user, $model));
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_denies_unauthorized_user_from_performing_action(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = new User(['id' => 99]);
|
||||
|
||||
// Act & Assert
|
||||
// $this->assertFalse($this->policy->update($user, $model));
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\{{Namespace}};
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use {{TargetClassFullName}};
|
||||
// use {{DependencyInterfaceFullName}};
|
||||
|
||||
class {{ClassName}}Test extends TestCase
|
||||
{
|
||||
// private {{DependencyInterface}}&MockInterface $dependencyMock;
|
||||
private {{ClassName}} $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// $this->dependencyMock = Mockery::mock({{DependencyInterface}}::class);
|
||||
// $this->service = new {{ClassName}}($this->dependencyMock);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_executes_successfully_under_normal_conditions(): void
|
||||
{
|
||||
// 1. Arrange
|
||||
|
||||
// 2. Act
|
||||
|
||||
// 3. Assert
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_throws_exception_when_input_is_invalid(): void
|
||||
{
|
||||
// 1. Arrange
|
||||
|
||||
// 2. Assert Expectation
|
||||
// $this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
// 3. Act
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user