cc-codeconductor 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +2642 -226
- package/package.json +1 -1
- package/presets/agy/skills/evaluation/SKILL.md +6 -0
- package/presets/agy/skills/openspec/SKILL.md +32 -0
- package/presets/agy/workflows/cc-openspec.md +62 -0
- package/presets/agy/workflows/cc-scorecard.md +17 -0
- package/presets/claude/commands/cc/openspec.md +144 -0
- package/presets/claude/commands/cc/review.md +13 -2
- package/presets/claude/commands/cc/scorecard.md +65 -0
- package/presets/claude/skills/evaluation/SKILL.md +42 -0
- package/presets/claude/skills/openspec/SKILL.md +54 -0
- package/presets/codex/AGENTS.md +57 -0
- package/presets/cursor/.cursorignore +15 -0
- package/presets/cursor/AGENTS.md +504 -0
- package/presets/cursor/agents/architect.md +211 -0
- package/presets/cursor/agents/complexity-auditor.md +76 -0
- package/presets/cursor/agents/contract-builder.md +75 -0
- package/presets/cursor/agents/docs.md +180 -0
- package/presets/cursor/agents/goal-planner.md +71 -0
- package/presets/cursor/agents/implementer.md +161 -0
- package/presets/cursor/agents/orchestrator.md +377 -0
- package/presets/cursor/agents/repo-explorer.md +100 -0
- package/presets/cursor/agents/reviewer.md +237 -0
- package/presets/cursor/agents/security-reviewer.md +113 -0
- package/presets/cursor/agents/task-coach.md +145 -0
- package/presets/cursor/agents/tester.md +241 -0
- package/presets/cursor/commands/cc/api-contract.md +58 -0
- package/presets/cursor/commands/cc/db-migration.md +58 -0
- package/presets/cursor/commands/cc/feature.md +115 -0
- package/presets/cursor/commands/cc/fix.md +121 -0
- package/presets/cursor/commands/cc/openspec.md +144 -0
- package/presets/cursor/commands/cc/pagespeed.md +103 -0
- package/presets/cursor/commands/cc/refactor.md +148 -0
- package/presets/cursor/commands/cc/review.md +137 -0
- package/presets/cursor/commands/cc/scorecard.md +65 -0
- package/presets/cursor/commands/cc/tdd-cycle.md +226 -0
- package/presets/cursor/commands/cc/test-plan.md +138 -0
- package/presets/cursor/rules/behavioral-discipline.mdc +14 -0
- package/presets/cursor/rules/context-budget.mdc +12 -0
- package/presets/cursor/rules/orchestration.mdc +12 -0
- package/presets/cursor/rules/yagni-stdlib.mdc +11 -0
- package/presets/cursor/skills/android/SKILL.md +122 -0
- package/presets/cursor/skills/api-versioning/SKILL.md +394 -0
- package/presets/cursor/skills/astro/SKILL.md +322 -0
- package/presets/cursor/skills/auth-token-inspector/SKILL.md +33 -0
- package/presets/cursor/skills/code-review/SKILL.md +208 -0
- package/presets/cursor/skills/conductor-setup/SKILL.md +127 -0
- package/presets/cursor/skills/django-orm/SKILL.md +463 -0
- package/presets/cursor/skills/django-testing/SKILL.md +417 -0
- package/presets/cursor/skills/django-uv/SKILL.md +409 -0
- package/presets/cursor/skills/drizzle-schema-architect/SKILL.md +54 -0
- package/presets/cursor/skills/evaluation/SKILL.md +8 -0
- package/presets/cursor/skills/fastapi-pydantic-strict/SKILL.md +46 -0
- package/presets/cursor/skills/find-skills/SKILL.md +144 -0
- package/presets/cursor/skills/jpa-nplusone-detector/SKILL.md +49 -0
- package/presets/cursor/skills/jpa-postgres/SKILL.md +626 -0
- package/presets/cursor/skills/laravel-specialist/SKILL.md +267 -0
- package/presets/cursor/skills/laravel-specialist/references/eloquent.md +351 -0
- package/presets/cursor/skills/laravel-specialist/references/livewire.md +512 -0
- package/presets/cursor/skills/laravel-specialist/references/queues.md +423 -0
- package/presets/cursor/skills/laravel-specialist/references/routing.md +362 -0
- package/presets/cursor/skills/laravel-specialist/references/testing.md +522 -0
- package/presets/cursor/skills/livewire-alpine-bridge/SKILL.md +39 -0
- package/presets/cursor/skills/multi-agent-orchestration/README.md +144 -0
- package/presets/cursor/skills/multi-agent-orchestration/SKILL.md +579 -0
- package/presets/cursor/skills/multi-agent-orchestration/examples/framework_implementations.py +362 -0
- package/presets/cursor/skills/multi-agent-orchestration/examples/orchestration_patterns.py +411 -0
- package/presets/cursor/skills/multi-agent-orchestration/scripts/agent_communication.py +334 -0
- package/presets/cursor/skills/multi-agent-orchestration/scripts/benchmarking.py +341 -0
- package/presets/cursor/skills/multi-agent-orchestration/scripts/workflow_management.py +334 -0
- package/presets/cursor/skills/nextjs-typescript/SKILL.md +394 -0
- package/presets/cursor/skills/openspec/SKILL.md +52 -0
- package/presets/cursor/skills/pagespeed-insights/SKILL.md +445 -0
- package/presets/cursor/skills/pagespeed-insights/reference.md +50 -0
- package/presets/cursor/skills/pagespeed-perf/SKILL.md +281 -0
- package/presets/cursor/skills/php-pro/SKILL.md +210 -0
- package/presets/cursor/skills/php-pro/references/async-patterns.md +412 -0
- package/presets/cursor/skills/php-pro/references/laravel-patterns.md +377 -0
- package/presets/cursor/skills/php-pro/references/modern-php-features.md +323 -0
- package/presets/cursor/skills/php-pro/references/symfony-patterns.md +466 -0
- package/presets/cursor/skills/php-pro/references/testing-quality.md +466 -0
- package/presets/cursor/skills/python/SKILL.md +613 -0
- package/presets/cursor/skills/python-django-stack/SKILL.md +500 -0
- package/presets/cursor/skills/python-fastapi-stack/SKILL.md +464 -0
- package/presets/cursor/skills/security/SKILL.md +384 -0
- package/presets/cursor/skills/seo-analytics-injector/SKILL.md +44 -0
- package/presets/cursor/skills/spring-auth-auditor/SKILL.md +33 -0
- package/presets/cursor/skills/spring-boot-feature/SKILL.md +566 -0
- package/presets/cursor/skills/spring-boot-kotlin/SKILL.md +408 -0
- package/presets/cursor/skills/spring-boot-testing-strategy/SKILL.md +479 -0
- package/presets/cursor/skills/sqlalchemy/SKILL.md +473 -0
- package/presets/cursor/skills/tailwind-responsive-auditor/SKILL.md +32 -0
- package/presets/cursor/skills/tdd-mutation-tester/SKILL.md +28 -0
- package/presets/cursor/skills/testing-tdd/SKILL.md +592 -0
- package/presets/cursor/skills/workflow-orchestration-patterns/SKILL.md +98 -0
- package/presets/cursor/skills/workflow-orchestration-patterns/references/details.md +223 -0
- package/presets/opencode/agents/architect.md +1 -2
- package/presets/opencode/agents/complexity-auditor.md +1 -0
- package/presets/opencode/agents/contract-builder.md +93 -0
- package/presets/opencode/agents/docs.md +1 -2
- package/presets/opencode/agents/goal-planner.md +82 -0
- package/presets/opencode/agents/implementer.md +9 -2
- package/presets/opencode/agents/orchestrator.md +50 -12
- package/presets/opencode/agents/repo-explorer.md +0 -1
- package/presets/opencode/agents/reviewer.md +23 -2
- package/presets/opencode/agents/security-reviewer.md +129 -0
- package/presets/opencode/agents/task-coach.md +1 -2
- package/presets/opencode/agents/tester.md +1 -2
- package/presets/opencode/commands/cc-openspec.md +61 -0
- package/presets/opencode/commands/cc-scorecard.md +16 -0
- package/presets/opencode/prompts/v0.5.0/architect.md +222 -0
- package/presets/opencode/prompts/v0.5.0/complexity-auditor.md +91 -0
- package/presets/opencode/prompts/v0.5.0/contract-builder.md +84 -0
- package/presets/opencode/prompts/v0.5.0/docs.md +190 -0
- package/presets/opencode/prompts/v0.5.0/goal-planner.md +80 -0
- package/presets/opencode/prompts/v0.5.0/implementer.md +171 -0
- package/presets/opencode/prompts/v0.5.0/orchestrator.md +388 -0
- package/presets/opencode/prompts/v0.5.0/repo-explorer.md +111 -0
- package/presets/opencode/prompts/v0.5.0/reviewer.md +248 -0
- package/presets/opencode/prompts/v0.5.0/security-reviewer.md +123 -0
- package/presets/opencode/prompts/v0.5.0/task-coach.md +156 -0
- package/presets/opencode/prompts/v0.5.0/tester.md +252 -0
- package/presets/opencode/skills/evaluation/SKILL.md +6 -0
- package/presets/opencode/skills/openspec/SKILL.md +50 -0
- package/presets/templates/BACKLOG.md +33 -0
- package/presets/templates/execution-profile.yml +6 -0
- package/presets/templates/model-comparison.md +11 -0
- package/presets/templates/regression-checklist.yml +10 -0
- package/src/presets/manifests/agy.yml +2 -2
- package/src/presets/manifests/claude.yml +2 -2
- package/src/presets/manifests/codex.yml +2 -2
- package/src/presets/manifests/cursor.yml +19 -3
- package/src/presets/manifests/gemini.yml +2 -2
- package/src/presets/manifests/opencode.yml +2 -2
- package/src/presets/models/agy.yml +21 -0
- package/src/presets/models/claude.yml +18 -0
- package/src/presets/models/codex.yml +18 -0
- package/src/presets/models/cursor.yml +39 -9
- package/src/presets/models/gemini.yml +18 -0
- package/src/presets/models/opencode.yml +18 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# Testing & Quality Assurance
|
|
2
|
+
|
|
3
|
+
## PHPUnit with Strict Types
|
|
4
|
+
|
|
5
|
+
```php
|
|
6
|
+
<?php
|
|
7
|
+
|
|
8
|
+
declare(strict_types=1);
|
|
9
|
+
|
|
10
|
+
namespace Tests\Unit\Service;
|
|
11
|
+
|
|
12
|
+
use App\Repository\UserRepositoryInterface;
|
|
13
|
+
use App\Service\UserService;
|
|
14
|
+
use App\Service\EmailService;
|
|
15
|
+
use PHPUnit\Framework\TestCase;
|
|
16
|
+
use PHPUnit\Framework\MockObject\MockObject;
|
|
17
|
+
|
|
18
|
+
final class UserServiceTest extends TestCase
|
|
19
|
+
{
|
|
20
|
+
private UserRepositoryInterface&MockObject $userRepository;
|
|
21
|
+
private EmailService&MockObject $emailService;
|
|
22
|
+
private UserService $userService;
|
|
23
|
+
|
|
24
|
+
protected function setUp(): void
|
|
25
|
+
{
|
|
26
|
+
$this->userRepository = $this->createMock(UserRepositoryInterface::class);
|
|
27
|
+
$this->emailService = $this->createMock(EmailService::class);
|
|
28
|
+
$this->userService = new UserService(
|
|
29
|
+
$this->userRepository,
|
|
30
|
+
$this->emailService
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public function testCreateUserSuccessfully(): void
|
|
35
|
+
{
|
|
36
|
+
$email = 'test@example.com';
|
|
37
|
+
$password = 'SecurePass123!';
|
|
38
|
+
|
|
39
|
+
$this->userRepository
|
|
40
|
+
->expects($this->once())
|
|
41
|
+
->method('findByEmail')
|
|
42
|
+
->with($email)
|
|
43
|
+
->willReturn(null);
|
|
44
|
+
|
|
45
|
+
$this->userRepository
|
|
46
|
+
->expects($this->once())
|
|
47
|
+
->method('create')
|
|
48
|
+
->willReturn($this->createUser($email));
|
|
49
|
+
|
|
50
|
+
$this->emailService
|
|
51
|
+
->expects($this->once())
|
|
52
|
+
->method('sendWelcomeEmail');
|
|
53
|
+
|
|
54
|
+
$user = $this->userService->createUser($email, $password);
|
|
55
|
+
|
|
56
|
+
$this->assertSame($email, $user->email);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public function testCreateUserThrowsExceptionWhenEmailExists(): void
|
|
60
|
+
{
|
|
61
|
+
$this->expectException(\DomainException::class);
|
|
62
|
+
$this->expectExceptionMessage('Email already exists');
|
|
63
|
+
|
|
64
|
+
$this->userRepository
|
|
65
|
+
->method('findByEmail')
|
|
66
|
+
->willReturn($this->createUser('test@example.com'));
|
|
67
|
+
|
|
68
|
+
$this->userService->createUser('test@example.com', 'password');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private function createUser(string $email): User
|
|
72
|
+
{
|
|
73
|
+
return new User(
|
|
74
|
+
id: 1,
|
|
75
|
+
email: $email,
|
|
76
|
+
password: password_hash('password', PASSWORD_ARGON2ID),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Data Providers
|
|
83
|
+
|
|
84
|
+
```php
|
|
85
|
+
<?php
|
|
86
|
+
|
|
87
|
+
declare(strict_types=1);
|
|
88
|
+
|
|
89
|
+
namespace Tests\Unit\Validator;
|
|
90
|
+
|
|
91
|
+
use App\Validator\EmailValidator;
|
|
92
|
+
use PHPUnit\Framework\Attributes\DataProvider;
|
|
93
|
+
use PHPUnit\Framework\Attributes\Test;
|
|
94
|
+
use PHPUnit\Framework\TestCase;
|
|
95
|
+
|
|
96
|
+
final class EmailValidatorTest extends TestCase
|
|
97
|
+
{
|
|
98
|
+
#[Test]
|
|
99
|
+
#[DataProvider('validEmailProvider')]
|
|
100
|
+
public function itValidatesCorrectEmails(string $email): void
|
|
101
|
+
{
|
|
102
|
+
$validator = new EmailValidator();
|
|
103
|
+
$this->assertTrue($validator->isValid($email));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[Test]
|
|
107
|
+
#[DataProvider('invalidEmailProvider')]
|
|
108
|
+
public function itRejectsInvalidEmails(string $email): void
|
|
109
|
+
{
|
|
110
|
+
$validator = new EmailValidator();
|
|
111
|
+
$this->assertFalse($validator->isValid($email));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public static function validEmailProvider(): array
|
|
115
|
+
{
|
|
116
|
+
return [
|
|
117
|
+
['user@example.com'],
|
|
118
|
+
['john.doe@company.co.uk'],
|
|
119
|
+
['test+filter@domain.org'],
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public static function invalidEmailProvider(): array
|
|
124
|
+
{
|
|
125
|
+
return [
|
|
126
|
+
['invalid'],
|
|
127
|
+
['@example.com'],
|
|
128
|
+
['user@'],
|
|
129
|
+
['user space@example.com'],
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Laravel Feature Tests
|
|
136
|
+
|
|
137
|
+
```php
|
|
138
|
+
<?php
|
|
139
|
+
|
|
140
|
+
declare(strict_types=1);
|
|
141
|
+
|
|
142
|
+
namespace Tests\Feature;
|
|
143
|
+
|
|
144
|
+
use App\Models\User;
|
|
145
|
+
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
146
|
+
use Illuminate\Foundation\Testing\WithFaker;
|
|
147
|
+
use Tests\TestCase;
|
|
148
|
+
|
|
149
|
+
final class UserControllerTest extends TestCase
|
|
150
|
+
{
|
|
151
|
+
use RefreshDatabase, WithFaker;
|
|
152
|
+
|
|
153
|
+
public function testUserCanViewTheirProfile(): void
|
|
154
|
+
{
|
|
155
|
+
$user = User::factory()->create();
|
|
156
|
+
|
|
157
|
+
$response = $this->actingAs($user)->get('/api/users/me');
|
|
158
|
+
|
|
159
|
+
$response->assertOk()
|
|
160
|
+
->assertJson([
|
|
161
|
+
'data' => [
|
|
162
|
+
'id' => $user->id,
|
|
163
|
+
'email' => $user->email,
|
|
164
|
+
],
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public function testUserCanUpdateTheirProfile(): void
|
|
169
|
+
{
|
|
170
|
+
$user = User::factory()->create();
|
|
171
|
+
$newName = $this->faker->name();
|
|
172
|
+
|
|
173
|
+
$response = $this->actingAs($user)->putJson('/api/users/me', [
|
|
174
|
+
'name' => $newName,
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
$response->assertOk();
|
|
178
|
+
|
|
179
|
+
$this->assertDatabaseHas('users', [
|
|
180
|
+
'id' => $user->id,
|
|
181
|
+
'name' => $newName,
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public function testUnauthorizedUserCannotAccessProfile(): void
|
|
186
|
+
{
|
|
187
|
+
$response = $this->getJson('/api/users/me');
|
|
188
|
+
|
|
189
|
+
$response->assertUnauthorized();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public function testValidationFailsWithInvalidData(): void
|
|
193
|
+
{
|
|
194
|
+
$user = User::factory()->create();
|
|
195
|
+
|
|
196
|
+
$response = $this->actingAs($user)->putJson('/api/users/me', [
|
|
197
|
+
'email' => 'not-an-email',
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
$response->assertUnprocessable()
|
|
201
|
+
->assertJsonValidationErrors(['email']);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Pest Testing (Modern Alternative)
|
|
207
|
+
|
|
208
|
+
```php
|
|
209
|
+
<?php
|
|
210
|
+
|
|
211
|
+
declare(strict_types=1);
|
|
212
|
+
|
|
213
|
+
use App\Models\User;
|
|
214
|
+
use App\Services\UserService;
|
|
215
|
+
|
|
216
|
+
beforeEach(function () {
|
|
217
|
+
$this->userService = app(UserService::class);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('creates a user successfully', function () {
|
|
221
|
+
$user = $this->userService->createUser(
|
|
222
|
+
email: 'test@example.com',
|
|
223
|
+
password: 'SecurePass123!'
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect($user)
|
|
227
|
+
->toBeInstanceOf(User::class)
|
|
228
|
+
->email->toBe('test@example.com');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('validates email format', function (string $email, bool $valid) {
|
|
232
|
+
$validator = new EmailValidator();
|
|
233
|
+
|
|
234
|
+
expect($validator->isValid($email))->toBe($valid);
|
|
235
|
+
})->with([
|
|
236
|
+
['test@example.com', true],
|
|
237
|
+
['invalid', false],
|
|
238
|
+
['@example.com', false],
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
test('authenticated user can view profile', function () {
|
|
242
|
+
$user = User::factory()->create();
|
|
243
|
+
|
|
244
|
+
$this->actingAs($user)
|
|
245
|
+
->get('/api/users/me')
|
|
246
|
+
->assertOk()
|
|
247
|
+
->assertJson(['data' => ['email' => $user->email]]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('guest cannot access protected routes', function () {
|
|
251
|
+
$this->getJson('/api/users/me')
|
|
252
|
+
->assertUnauthorized();
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## PHPStan Configuration
|
|
257
|
+
|
|
258
|
+
```neon
|
|
259
|
+
# phpstan.neon
|
|
260
|
+
parameters:
|
|
261
|
+
level: 9
|
|
262
|
+
paths:
|
|
263
|
+
- src
|
|
264
|
+
- tests
|
|
265
|
+
excludePaths:
|
|
266
|
+
- src/bootstrap.php
|
|
267
|
+
- vendor
|
|
268
|
+
checkMissingIterableValueType: true
|
|
269
|
+
checkGenericClassInNonGenericObjectType: true
|
|
270
|
+
reportUnmatchedIgnoredErrors: true
|
|
271
|
+
tmpDir: var/cache/phpstan
|
|
272
|
+
|
|
273
|
+
ignoreErrors:
|
|
274
|
+
# Ignore specific Laravel magic
|
|
275
|
+
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder#'
|
|
276
|
+
|
|
277
|
+
type_coverage:
|
|
278
|
+
return_type: 100
|
|
279
|
+
param_type: 100
|
|
280
|
+
property_type: 100
|
|
281
|
+
|
|
282
|
+
includes:
|
|
283
|
+
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
|
284
|
+
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## PHPStan Annotations
|
|
288
|
+
|
|
289
|
+
```php
|
|
290
|
+
<?php
|
|
291
|
+
|
|
292
|
+
declare(strict_types=1);
|
|
293
|
+
|
|
294
|
+
namespace App\Repository;
|
|
295
|
+
|
|
296
|
+
use App\Entity\User;
|
|
297
|
+
use Doctrine\ORM\EntityRepository;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @extends EntityRepository<User>
|
|
301
|
+
*/
|
|
302
|
+
final class UserRepository extends EntityRepository
|
|
303
|
+
{
|
|
304
|
+
/**
|
|
305
|
+
* @return User[]
|
|
306
|
+
*/
|
|
307
|
+
public function findActive(): array
|
|
308
|
+
{
|
|
309
|
+
return $this->createQueryBuilder('u')
|
|
310
|
+
->where('u.status = :status')
|
|
311
|
+
->setParameter('status', 'active')
|
|
312
|
+
->getQuery()
|
|
313
|
+
->getResult();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @param int[] $ids
|
|
318
|
+
* @return User[]
|
|
319
|
+
*/
|
|
320
|
+
public function findByIds(array $ids): array
|
|
321
|
+
{
|
|
322
|
+
return $this->createQueryBuilder('u')
|
|
323
|
+
->where('u.id IN (:ids)')
|
|
324
|
+
->setParameter('ids', $ids)
|
|
325
|
+
->getQuery()
|
|
326
|
+
->getResult();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @template T
|
|
332
|
+
*/
|
|
333
|
+
final readonly class Result
|
|
334
|
+
{
|
|
335
|
+
/**
|
|
336
|
+
* @param T $data
|
|
337
|
+
*/
|
|
338
|
+
public function __construct(
|
|
339
|
+
public mixed $data,
|
|
340
|
+
public bool $success,
|
|
341
|
+
) {}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @return T
|
|
345
|
+
*/
|
|
346
|
+
public function getData(): mixed
|
|
347
|
+
{
|
|
348
|
+
return $this->data;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Mockery (Advanced Mocking)
|
|
354
|
+
|
|
355
|
+
```php
|
|
356
|
+
<?php
|
|
357
|
+
|
|
358
|
+
declare(strict_types=1);
|
|
359
|
+
|
|
360
|
+
namespace Tests\Unit\Service;
|
|
361
|
+
|
|
362
|
+
use App\Repository\UserRepository;
|
|
363
|
+
use App\Service\NotificationService;
|
|
364
|
+
use Mockery;
|
|
365
|
+
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
|
|
366
|
+
use PHPUnit\Framework\TestCase;
|
|
367
|
+
|
|
368
|
+
final class NotificationServiceTest extends TestCase
|
|
369
|
+
{
|
|
370
|
+
use MockeryPHPUnitIntegration;
|
|
371
|
+
|
|
372
|
+
public function testSendsNotificationToActiveUsers(): void
|
|
373
|
+
{
|
|
374
|
+
$repository = Mockery::mock(UserRepository::class);
|
|
375
|
+
$repository->shouldReceive('findActive')
|
|
376
|
+
->once()
|
|
377
|
+
->andReturn([
|
|
378
|
+
$this->createUser('user1@example.com'),
|
|
379
|
+
$this->createUser('user2@example.com'),
|
|
380
|
+
]);
|
|
381
|
+
|
|
382
|
+
$service = new NotificationService($repository);
|
|
383
|
+
$result = $service->notifyActiveUsers('Important message');
|
|
384
|
+
|
|
385
|
+
$this->assertSame(2, $result->count());
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
public function testHandlesEmailServiceFailure(): void
|
|
389
|
+
{
|
|
390
|
+
$emailService = Mockery::mock(EmailService::class);
|
|
391
|
+
$emailService->shouldReceive('send')
|
|
392
|
+
->once()
|
|
393
|
+
->andThrow(new \RuntimeException('Email service down'));
|
|
394
|
+
|
|
395
|
+
$service = new NotificationService($emailService);
|
|
396
|
+
|
|
397
|
+
$this->expectException(\RuntimeException::class);
|
|
398
|
+
$service->sendNotification('test@example.com', 'Hello');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private function createUser(string $email): User
|
|
402
|
+
{
|
|
403
|
+
return new User(id: 1, email: $email, password: 'hashed');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Code Coverage
|
|
409
|
+
|
|
410
|
+
```xml
|
|
411
|
+
<!-- phpunit.xml -->
|
|
412
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
413
|
+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
414
|
+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
|
415
|
+
bootstrap="vendor/autoload.php"
|
|
416
|
+
colors="true"
|
|
417
|
+
failOnRisky="true"
|
|
418
|
+
failOnWarning="true"
|
|
419
|
+
stopOnFailure="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
|
+
```
|
|
448
|
+
|
|
449
|
+
## Quick Reference
|
|
450
|
+
|
|
451
|
+
| 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` |
|
|
459
|
+
|
|
460
|
+
| 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()` |
|