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,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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
"
|
||||
Executable
+64
@@ -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
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Executable
+107
@@ -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}"
|
||||
Reference in New Issue
Block a user