Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Modern PHP 8.3+ development with strict typing, PHPStan level 9, PSR standards, and Laravel/Symfony support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/testing-quality.md
1# Testing & Quality Assurance23## PHPUnit with Strict Types45```php6<?php78declare(strict_types=1);910namespace Tests\Unit\Service;1112use App\Repository\UserRepositoryInterface;13use App\Service\UserService;14use App\Service\EmailService;15use PHPUnit\Framework\TestCase;16use PHPUnit\Framework\MockObject\MockObject;1718final class UserServiceTest extends TestCase19{20private UserRepositoryInterface&MockObject $userRepository;21private EmailService&MockObject $emailService;22private UserService $userService;2324protected function setUp(): void25{26$this->userRepository = $this->createMock(UserRepositoryInterface::class);27$this->emailService = $this->createMock(EmailService::class);28$this->userService = new UserService(29$this->userRepository,30$this->emailService31);32}3334public function testCreateUserSuccessfully(): void35{36$email = '[email protected]';37$password = 'SecurePass123!';3839$this->userRepository40->expects($this->once())41->method('findByEmail')42->with($email)43->willReturn(null);4445$this->userRepository46->expects($this->once())47->method('create')48->willReturn($this->createUser($email));4950$this->emailService51->expects($this->once())52->method('sendWelcomeEmail');5354$user = $this->userService->createUser($email, $password);5556$this->assertSame($email, $user->email);57}5859public function testCreateUserThrowsExceptionWhenEmailExists(): void60{61$this->expectException(\DomainException::class);62$this->expectExceptionMessage('Email already exists');6364$this->userRepository65->method('findByEmail')66->willReturn($this->createUser('[email protected]'));6768$this->userService->createUser('[email protected]', 'password');69}7071private function createUser(string $email): User72{73return new User(74id: 1,75email: $email,76password: password_hash('password', PASSWORD_ARGON2ID),77);78}79}80```8182## Data Providers8384```php85<?php8687declare(strict_types=1);8889namespace Tests\Unit\Validator;9091use App\Validator\EmailValidator;92use PHPUnit\Framework\Attributes\DataProvider;93use PHPUnit\Framework\Attributes\Test;94use PHPUnit\Framework\TestCase;9596final class EmailValidatorTest extends TestCase97{98#[Test]99#[DataProvider('validEmailProvider')]100public function itValidatesCorrectEmails(string $email): void101{102$validator = new EmailValidator();103$this->assertTrue($validator->isValid($email));104}105106#[Test]107#[DataProvider('invalidEmailProvider')]108public function itRejectsInvalidEmails(string $email): void109{110$validator = new EmailValidator();111$this->assertFalse($validator->isValid($email));112}113114public static function validEmailProvider(): array115{116return [117['[email protected]'],118['[email protected]'],119['[email protected]'],120];121}122123public static function invalidEmailProvider(): array124{125return [126['invalid'],127['@example.com'],128['user@'],129['user [email protected]'],130];131}132}133```134135## Laravel Feature Tests136137```php138<?php139140declare(strict_types=1);141142namespace Tests\Feature;143144use App\Models\User;145use Illuminate\Foundation\Testing\RefreshDatabase;146use Illuminate\Foundation\Testing\WithFaker;147use Tests\TestCase;148149final class UserControllerTest extends TestCase150{151use RefreshDatabase, WithFaker;152153public function testUserCanViewTheirProfile(): void154{155$user = User::factory()->create();156157$response = $this->actingAs($user)->get('/api/users/me');158159$response->assertOk()160->assertJson([161'data' => [162'id' => $user->id,163'email' => $user->email,164],165]);166}167168public function testUserCanUpdateTheirProfile(): void169{170$user = User::factory()->create();171$newName = $this->faker->name();172173$response = $this->actingAs($user)->putJson('/api/users/me', [174'name' => $newName,175]);176177$response->assertOk();178179$this->assertDatabaseHas('users', [180'id' => $user->id,181'name' => $newName,182]);183}184185public function testUnauthorizedUserCannotAccessProfile(): void186{187$response = $this->getJson('/api/users/me');188189$response->assertUnauthorized();190}191192public function testValidationFailsWithInvalidData(): void193{194$user = User::factory()->create();195196$response = $this->actingAs($user)->putJson('/api/users/me', [197'email' => 'not-an-email',198]);199200$response->assertUnprocessable()201->assertJsonValidationErrors(['email']);202}203}204```205206## Pest Testing (Modern Alternative)207208```php209<?php210211declare(strict_types=1);212213use App\Models\User;214use App\Services\UserService;215216beforeEach(function () {217$this->userService = app(UserService::class);218});219220it('creates a user successfully', function () {221$user = $this->userService->createUser(222email: '[email protected]',223password: 'SecurePass123!'224);225226expect($user)227->toBeInstanceOf(User::class)228->email->toBe('[email protected]');229});230231it('validates email format', function (string $email, bool $valid) {232$validator = new EmailValidator();233234expect($validator->isValid($email))->toBe($valid);235})->with([236['[email protected]', true],237['invalid', false],238['@example.com', false],239]);240241test('authenticated user can view profile', function () {242$user = User::factory()->create();243244$this->actingAs($user)245->get('/api/users/me')246->assertOk()247->assertJson(['data' => ['email' => $user->email]]);248});249250test('guest cannot access protected routes', function () {251$this->getJson('/api/users/me')252->assertUnauthorized();253});254```255256## PHPStan Configuration257258```neon259# phpstan.neon260parameters:261level: 9262paths:263- src264- tests265excludePaths:266- src/bootstrap.php267- vendor268checkMissingIterableValueType: true269checkGenericClassInNonGenericObjectType: true270reportUnmatchedIgnoredErrors: true271tmpDir: var/cache/phpstan272273ignoreErrors:274# Ignore specific Laravel magic275- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder#'276277type_coverage:278return_type: 100279param_type: 100280property_type: 100281282includes:283- vendor/phpstan/phpstan-strict-rules/rules.neon284- vendor/phpstan/phpstan-deprecation-rules/rules.neon285```286287## PHPStan Annotations288289```php290<?php291292declare(strict_types=1);293294namespace App\Repository;295296use App\Entity\User;297use Doctrine\ORM\EntityRepository;298299/**300* @extends EntityRepository<User>301*/302final class UserRepository extends EntityRepository303{304/**305* @return User[]306*/307public function findActive(): array308{309return $this->createQueryBuilder('u')310->where('u.status = :status')311->setParameter('status', 'active')312->getQuery()313->getResult();314}315316/**317* @param int[] $ids318* @return User[]319*/320public function findByIds(array $ids): array321{322return $this->createQueryBuilder('u')323->where('u.id IN (:ids)')324->setParameter('ids', $ids)325->getQuery()326->getResult();327}328}329330/**331* @template T332*/333final readonly class Result334{335/**336* @param T $data337*/338public function __construct(339public mixed $data,340public bool $success,341) {}342343/**344* @return T345*/346public function getData(): mixed347{348return $this->data;349}350}351```352353## Mockery (Advanced Mocking)354355```php356<?php357358declare(strict_types=1);359360namespace Tests\Unit\Service;361362use App\Repository\UserRepository;363use App\Service\NotificationService;364use Mockery;365use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;366use PHPUnit\Framework\TestCase;367368final class NotificationServiceTest extends TestCase369{370use MockeryPHPUnitIntegration;371372public function testSendsNotificationToActiveUsers(): void373{374$repository = Mockery::mock(UserRepository::class);375$repository->shouldReceive('findActive')376->once()377->andReturn([378$this->createUser('[email protected]'),379$this->createUser('[email protected]'),380]);381382$service = new NotificationService($repository);383$result = $service->notifyActiveUsers('Important message');384385$this->assertSame(2, $result->count());386}387388public function testHandlesEmailServiceFailure(): void389{390$emailService = Mockery::mock(EmailService::class);391$emailService->shouldReceive('send')392->once()393->andThrow(new \RuntimeException('Email service down'));394395$service = new NotificationService($emailService);396397$this->expectException(\RuntimeException::class);398$service->sendNotification('[email protected]', 'Hello');399}400401private function createUser(string $email): User402{403return new User(id: 1, email: $email, password: 'hashed');404}405}406```407408## Code Coverage409410```xml411<!-- phpunit.xml -->412<?xml version="1.0" encoding="UTF-8"?>413<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"414xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"415bootstrap="vendor/autoload.php"416colors="true"417failOnRisky="true"418failOnWarning="true"419stopOnFailure="false">420<testsuites>421<testsuite name="Unit">422<directory>tests/Unit</directory>423</testsuite>424<testsuite name="Feature">425<directory>tests/Feature</directory>426</testsuite>427</testsuites>428<coverage>429<include>430<directory suffix=".php">src</directory>431</include>432<exclude>433<directory>src/bootstrap</directory>434<file>src/Kernel.php</file>435</exclude>436<report>437<html outputDirectory="coverage/html"/>438<clover outputFile="coverage/clover.xml"/>439</report>440</coverage>441<php>442<env name="APP_ENV" value="testing"/>443<env name="DB_CONNECTION" value="sqlite"/>444<env name="DB_DATABASE" value=":memory:"/>445</php>446</phpunit>447```448449## Quick Reference450451| Tool | Purpose | Command |452|------|---------|---------|453| PHPUnit | Unit/Feature tests | `./vendor/bin/phpunit` |454| Pest | Modern testing | `./vendor/bin/pest` |455| PHPStan | Static analysis | `./vendor/bin/phpstan analyse` |456| Psalm | Alternative static analysis | `./vendor/bin/psalm` |457| PHP-CS-Fixer | Code style | `./vendor/bin/php-cs-fixer fix` |458| PHPMD | Mess detector | `./vendor/bin/phpmd src text cleancode` |459460| Assertion | PHPUnit | Pest |461|-----------|---------|------|462| Equality | `$this->assertSame()` | `expect()->toBe()` |463| Type | `$this->assertInstanceOf()` | `expect()->toBeInstanceOf()` |464| Array | `$this->assertContains()` | `expect()->toContain()` |465| Exception | `$this->expectException()` | `expect()->toThrow()` |466| Count | `$this->assertCount()` | `expect()->toHaveCount()` |467