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:
2026-09-07 16:41:37 +07:00
commit 2045c856e9
60 changed files with 5248 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
name: Laravel Unit Tests & Quality Gate
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
unit-tests:
name: Unit Tests (PHP ${{ matrix.php }} - ${{ matrix.dependency-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4']
dependency-version: [prefer-stable]
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup PHP & Extensions
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, dom, fileinfo, sqlite3, pdo_sqlite, pcov
coverage: pcov
tools: composer:v2
- name: Cache Composer Dependencies
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-${{ matrix.php }}-
- name: Install Dependencies
run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-progress
- name: Setup Environment Variables
run: |
cp .env.example .env.testing 2>/dev/null || true
php artisan key:generate --env=testing
- name: Run Unit Tests (Parallel Execution with Coverage Check)
run: |
php artisan test --testsuite=Unit --parallel --coverage --min=80
env:
APP_ENV: testing
DB_CONNECTION: sqlite
DB_DATABASE: ":memory:"
CACHE_STORE: array
QUEUE_CONNECTION: sync
MAIL_MAILER: array
+34
View File
@@ -0,0 +1,34 @@
# GitLab CI/CD Pipeline for Laravel Unit Testing
image: php:8.3-cli-alpine
stages:
- test
variables:
APP_ENV: "testing"
DB_CONNECTION: "sqlite"
DB_DATABASE: ":memory:"
CACHE_STORE: "array"
QUEUE_CONNECTION: "sync"
MAIL_MAILER: "array"
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- vendor/
unit-test:
stage: test
before_script:
- apk add --no-cache sqlite-libs libzip-dev zip unzip git $PHPIZE_DEPS
- pecl install pcov && docker-php-ext-enable pcov
- docker-php-ext-install pdo pdo_sqlite zip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --prefer-dist --no-progress --no-interaction
script:
- php artisan test --testsuite=Unit --coverage --min=80
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
artifacts:
when: always
reports:
junit: phpunit-report.xml
+28
View File
@@ -0,0 +1,28 @@
# Môi trường cách ly cho Unit & Feature Tests Laravel
APP_NAME="Laravel Test"
APP_ENV=testing
APP_KEY=base64:FakeKeyForTestingPurposesOnly1234567890=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=null
LOG_LEVEL=emergency
# Database SQLite in-memory siêu tốc
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
# Tối ưu mã hóa mật khẩu khi chạy test
BCRYPT_ROUNDS=4
# Chặn gửi mail / queue / cache / session thật
CACHE_STORE=array
CACHE_DRIVER=array
QUEUE_CONNECTION=sync
SESSION_DRIVER=array
MAIL_MAILER=array
# Tắt các monitoring package
TELESCOPE_ENABLED=false
PULSE_ENABLED=false
HORIZON_PREFIX=testing-horizon
+45
View File
@@ -0,0 +1,45 @@
<?php
/*
|--------------------------------------------------------------------------
| Test Case Configuration
|--------------------------------------------------------------------------
|
| Định cấu hình TestCase mặc định cho Unit và Feature tests trong Pest PHP.
|
*/
uses(
Tests\TestCase::class,
// Illuminate\Foundation\Testing\RefreshDatabase::class, // Chỉ bật trong Feature Test
)->in('Feature');
uses(
Tests\TestCase::class,
)->in('Unit');
/*
|--------------------------------------------------------------------------
| Expectations Custom Helpers
|--------------------------------------------------------------------------
|
| Tạo các custom expectation mở rộng của Pest nếu cần.
|
*/
expect()->extend('toBeValidEmail', function () {
return $this->toBeString()->toMatch('/^.+@.+\..+$/');
});
/*
|--------------------------------------------------------------------------
| Functions Helpers
|--------------------------------------------------------------------------
|
| Hàm global hỗ trợ khởi tạo dữ liệu giả lập cho test.
|
*/
// function asUser($user = null) {
// return test()->actingAs($user ?? User::factory()->create());
// }
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
requireCoverageMetadata="false"
beStrictAboutCoverageMetadata="false"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<source restrictDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory>./app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>
+21
View File
@@ -0,0 +1,21 @@
services:
test-runner:
image: php:8.3-cli-alpine
container_name: laravel_unit_tester
working_dir: /var/www/html
volumes:
- ./:/var/www/html
environment:
- APP_ENV=testing
- DB_CONNECTION=sqlite
- DB_DATABASE=:memory:
- BCRYPT_ROUNDS=4
- CACHE_STORE=array
- QUEUE_CONNECTION=sync
- MAIL_MAILER=array
command: >
sh -c "
apk add --no-cache sqlite-libs libzip-dev zip unzip git &&
docker-php-ext-install pdo pdo_sqlite zip &&
php artisan test
"
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# ==============================================================================
# Smart Test Runner for Laravel
# Tự động nhận diện môi trường: Laravel Sail -> Docker Compose -> Local PHP
# ==============================================================================
set -e
# Màu sắc thông báo
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${YELLOW}🔍 Đang phát hiện môi trường chạy test...${NC}"
# Hàm chạy test
run_test() {
local runner="$1"
shift
echo -e "${GREEN}🚀 Chạy test bằng: ${runner} $*${NC}"
$runner "$@"
}
# 1. Kiểm tra Laravel Sail
if [ -f "./vendor/bin/sail" ] && docker info >/dev/null 2>&1; then
if docker ps --format '{{.Names}}' | grep -q "laravel.test"; then
echo -e "${GREEN}✓ Phát hiện Laravel Sail đang hoạt động.${NC}"
run_test "./vendor/bin/sail test" "$@"
exit 0
fi
fi
# 2. Kiểm tra Docker Compose tự cấu hình
if [ -f "docker-compose.yml" ] || [ -f "compose.yaml" ]; then
if docker info >/dev/null 2>&1; then
# Tìm container service chạy php
SERVICES=("app" "php" "php-fpm" "backend" "workspace" "web")
for svc in "${SERVICES[@]}"; do
if docker compose ps --services --filter "status=running" 2>/dev/null | grep -q "^${svc}$"; then
echo -e "${GREEN}✓ Phát hiện Docker Compose với service: '${svc}'${NC}"
run_test "docker compose exec -T ${svc} php artisan test" "$@"
exit 0
fi
done
fi
fi
# 3. Fallback về PHP CLI cục bộ
if command -v php >/dev/null 2>&1; then
echo -e "${YELLOW}ℹ Không tìm thấy container Docker đang chạy. Chuyển về PHP CLI máy local.${NC}"
# Kiểm tra extension pdo_sqlite
if ! php -m | grep -q -i sqlite; then
echo -e "${RED}⚠️ Cảnh báo: PHP CLI hiện tại chưa bật extension pdo_sqlite. Unit test dùng In-memory SQLite có thể bị lỗi!${NC}"
fi
run_test "php artisan test" "$@"
exit 0
fi
echo -e "${RED}❌ Không tìm thấy môi trường phù hợp (Sail, Docker Compose hoặc PHP CLI) để chạy test!${NC}"
exit 1
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use {{TargetClassFullName}};
beforeEach(function () {
// $this->action = new {{ClassName}}(...);
});
describe('{{ClassName}}', function () {
it('handles action invocation successfully', function () {
// Arrange
// Act
// $result = ($this->action)($dto);
// Assert
expect(true)->toBeTrue();
});
});
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/*
|--------------------------------------------------------------------------
| Pest PHP Architecture Test Suite (Enterprise Pro Level)
|--------------------------------------------------------------------------
|
| Bộ kiểm thử kiến trúc toàn diện bảo vệ cấu trúc mã nguồn không bị suy thoái.
| Chạy bằng lệnh: ./test-runner.sh tests/Unit/ArchitectureTest.php
|
*/
describe('Architecture & Code Quality Gates', function () {
test('All files must use strict types')
->expect('App')
->toUseStrictTypes();
test('No debugging functions left behind in codebase')
->expect(['dd', 'dump', 'ray', 'var_dump', 'print_r', 'die', 'exit'])
->not->toBeUsed();
test('Controllers naming convention & inheritance')
->expect('App\Http\Controllers')
->toHaveSuffix('Controller');
test('Controllers should not directly query Eloquent Models')
->expect('App\Http\Controllers')
->not->toUse('App\Models')
->ignoring('App\Http\Controllers\Auth');
test('Services must be final or readonly')
->expect('App\Services')
->toHaveSuffix('Service');
test('DTOs and Value Objects must be immutable (readonly)')
->expect('App\DTOs')
->toBeReadonly();
test('Custom Validation Rules must implement ValidationRule contract')
->expect('App\Rules')
->toImplement('Illuminate\Contracts\Validation\ValidationRule');
test('Jobs must be queueable')
->expect('App\Jobs')
->toImplement('Illuminate\Contracts\Queue\ShouldQueue');
test('Events must not have dependencies injected')
->expect('App\Events')
->classes()
->not->toHaveConstructor();
});
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use Illuminate\Console\Command;
describe('{{CommandSignature}} Command', function () {
it('executes artisan command successfully', function () {
$this->artisan('{{CommandSignature}}')
->assertExitCode(Command::SUCCESS);
});
});
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use {{TargetClassFullName}};
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
beforeEach(function () {
// $this->controller = new {{ClassName}}(...);
});
describe('{{ClassName}}', function () {
it('returns successful response', function () {
// Arrange
// $request = Request::create('/api/resource', 'GET');
// Act
// $response = $this->controller->index($request);
// Assert
// expect($response->getStatusCode())->toBe(Response::HTTP_OK);
expect(true)->toBeTrue();
});
});
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use {{TargetClassFullName}};
use Illuminate\Support\Facades\Validator;
describe('{{ClassName}}', function () {
it('authorizes request appropriately', function () {
$request = new {{ClassName}}();
expect($request->authorize())->toBeTrue();
});
it('passes validation with valid payload', function () {
$request = new {{ClassName}}();
$validator = Validator::make([
// Valid data here
], $request->rules());
expect($validator->fails())->toBeFalse();
});
it('fails validation when required fields are missing', function () {
$request = new {{ClassName}}();
$validator = Validator::make([], $request->rules());
expect($validator->fails())->toBeTrue();
});
});
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
use {{TargetClassFullName}};
describe('{{ClassName}}', function () {
it('handles job execution successfully', function () {
// Arrange
// $dependencyMock = Mockery::mock(Service::class);
// $job = new {{ClassName}}(...);
// Act
// $job->handle($dependencyMock);
// Assert
expect(true)->toBeTrue();
});
});
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use {{TargetClassFullName}};
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
beforeEach(function () {
$this->middleware = new {{ClassName}}();
});
describe('{{ClassName}}', function () {
it('passes request to next closure when authorized', function () {
$request = Request::create('/test-route', 'GET');
$nextCalled = false;
$next = function ($req) use (&$nextCalled) {
$nextCalled = true;
return response('OK', Response::HTTP_OK);
};
$response = $this->middleware->handle($request, $next);
expect($nextCalled)->toBeTrue();
expect($response->getStatusCode())->toBe(Response::HTTP_OK);
});
});
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use {{TargetClassFullName}};
use App\Models\User;
beforeEach(function () {
$this->policy = new {{ClassName}}();
});
describe('{{ClassName}}', function () {
it('allows authorized user to perform action', function () {
$user = new User(['id' => 1]);
// expect($this->policy->view($user, $model))->toBeTrue();
expect(true)->toBeTrue();
});
it('denies unauthorized user from performing action', function () {
$user = new User(['id' => 99]);
// expect($this->policy->update($user, $model))->toBeFalse();
expect(true)->toBeTrue();
});
});
+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use {{TargetClassFullName}};
beforeEach(function () {
// $this->dependencyMock = Mockery::mock(DependencyInterface::class);
// $this->service = new {{ClassName}}($this->dependencyMock);
});
describe('{{ClassName}}', function () {
it('executes successfully under normal conditions', function () {
// Arrange
// Act
// Assert
expect(true)->toBeTrue();
});
it('throws exception when input is invalid', function () {
// expect(fn () => $this->service->method(...))->toThrow(\InvalidArgumentException::class);
});
});
+40
View File
@@ -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());
}
}
+35
View File
@@ -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());
}
}
+43
View File
@@ -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
}
}
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# ==============================================================================
# Smart Dependency Installer for Laravel Testing
# Tự động phát hiện gói còn thiếu và cài đặt qua Docker (Sail / Compose) hoặc Local Composer
# ==============================================================================
set -e
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m'
echo -e "${BLUE}===================================================================${NC}"
echo -e "${BLUE} Tự Động Cài Đặt Thư Viện Cần Thiết Cho Unit Test Laravel${NC}"
echo -e "${BLUE}===================================================================${NC}"
if [ ! -f "composer.json" ]; then
echo -e "${RED}❌ Lỗi: Không tìm thấy file composer.json trong thư mục hiện tại!${NC}"
exit 1
fi
# 1. Xác định môi trường thực thi lệnh composer
COMPOSER_CMD=""
if [ -f "./vendor/bin/sail" ] && docker info >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -q "laravel.test"; then
echo -e "${GREEN}✓ Môi trường: Laravel Sail đang hoạt động.${NC}"
COMPOSER_CMD="./vendor/bin/sail composer"
elif [ -f "docker-compose.yml" ] || [ -f "compose.yaml" ]; then
SERVICES=("app" "php" "php-fpm" "backend" "workspace" "web")
for svc in "${SERVICES[@]}"; do
if docker compose ps --services --filter "status=running" 2>/dev/null | grep -q "^${svc}$"; then
echo -e "${GREEN}✓ Môi trường: Docker Compose (service: ${svc}).${NC}"
COMPOSER_CMD="docker compose exec -T ${svc} composer"
break
fi
done
fi
if [ -z "$COMPOSER_CMD" ]; then
if command -v composer >/dev/null 2>&1; then
echo -e "${YELLOW}ℹ Môi trường: Composer trên máy cục bộ (Local).${NC}"
COMPOSER_CMD="composer"
else
echo -e "${RED}❌ Lỗi: Không tìm thấy Composer trên máy hoặc trong container Docker!${NC}"
exit 1
fi
fi
# 2. Phân tích composer.json xem thiếu gói nào
echo -e "\n${YELLOW}🔍 Đang kiểm tra dependencies hiện có...${NC}"
PACKAGES_TO_INSTALL=()
# Kiểm tra Mockery
if ! grep -q '"mockery/mockery"' composer.json; then
PACKAGES_TO_INSTALL+=("mockery/mockery")
fi
# Kiểm tra Faker
if ! grep -q '"fakerphp/faker"' composer.json; then
PACKAGES_TO_INSTALL+=("fakerphp/faker")
fi
# Kiểm tra Test Runner (Pest vs PHPUnit)
IS_PEST=false
if grep -q '"pestphp/pest"' composer.json; then
IS_PEST=true
fi
if [ "$IS_PEST" = true ]; then
if ! grep -q '"pestphp/pest-plugin-laravel"' composer.json; then
PACKAGES_TO_INSTALL+=("pestphp/pest-plugin-laravel")
fi
fi
# Kiểm tra công cụ chạy song song (Paratest)
if ! grep -q '"brianium/paratest"' composer.json; then
PACKAGES_TO_INSTALL+=("brianium/paratest")
fi
# Kiểm tra Collision
if ! grep -q '"nunomaduro/collision"' composer.json; then
PACKAGES_TO_INSTALL+=("nunomaduro/collision")
fi
# 3. Tiến hành cài đặt
if [ ${#PACKAGES_TO_INSTALL[@]} -eq 0 ]; then
echo -e "${GREEN}🎉 Tuyệt vời! Dự án đã có đầy đủ các thư viện kiểm thử cần thiết!${NC}"
exit 0
fi
echo -e "${YELLOW}📦 Các gói cần cài đặt bổ sung:${NC}"
for pkg in "${PACKAGES_TO_INSTALL[@]}"; do
echo -e " - ${BLUE}$pkg${NC}"
done
INSTALL_COMMAND="$COMPOSER_CMD require --dev ${PACKAGES_TO_INSTALL[*]} --with-all-dependencies"
echo -e "\n${YELLOW}🚀 Đang thực thi lệnh cài đặt:${NC}"
echo -e " $INSTALL_COMMAND\n"
eval "$INSTALL_COMMAND"
echo -e "\n${GREEN}✅ Đã cài đặt hoàn tất các thư viện test cho dự án!${NC}"