← Back to list
writing-unit-tests
by moonpixels
Laravel skeleton application template with Inertia React.
⭐ 1🍴 0📅 Jan 10, 2026
SKILL.md
name: writing-unit-tests description: Write unit tests for isolated business logic, Actions, DTOs, Services, and domain code using Pest v4. SUPPLEMENTARY to feature tests - use for complex, concentrated areas of the codebase that benefit from focused isolated testing. Feature tests should be your primary approach.
Write Unit Tests
Unit tests are supplementary to feature tests for testing isolated business logic. Use them selectively for complex, concentrated areas like Action classes, Service classes, or helper functions that benefit from focused, isolated testing with mocked dependencies.
Testing Hierarchy
- Feature Tests (PRIMARY) - Default for all business logic and HTTP endpoints
- Unit Tests (SUPPLEMENTARY) - For complex, concentrated areas (you're here)
- Browser Tests (MINIMAL) - Only for core areas and JS interactions
File Structure
tests/Unit/{Domain}/{ClassName}Test.php
Examples:
tests/Unit/Support/Localisation/LocalisationTest.phptests/Unit/Helpers/HelpersTest.phptests/Unit/Models/UserTest.php
Core Conventions
Test Structure
<?php
declare(strict_types=1);
use App\Actions\Domain\MyAction;
use App\DTOs\Domain\MyData;
test('it can perform expected behavior', function (): void {
$action = createAction();
$data = MyData::from(['field' => 'value']);
$result = $action->handle($data);
expect($result)
->toBeInstanceOf(ExpectedClass::class)
->property->toBe('expected value');
});
// Helper functions at bottom of file
function createAction(): MyAction
{
return new MyAction(
dependency: mock(DependencyClass::class)
);
}
Key Requirements:
- Always include
declare(strict_types=1); - Use
test('it can...', function (): void {})syntax - Descriptive test names starting with "it can" or "it throws"
- Follow Arrange-Act-Assert (AAA) pattern
- Helper functions at bottom of file
- Avoid database when possible (use mocks/fakes)
Expect Assertions
// Value assertions
expect($value)->toBe('expected')
expect($value)->toBeTrue()
expect($value)->toBeNull()
// Type assertions
expect($value)->toBeInstanceOf(User::class)
// Collection assertions
expect($collection)->toHaveCount(3)
// Chaining assertions
expect($user)
->name->toBe('John Doe')
->email->toBe('john@example.com');
Exception Testing
test('it throws exception for invalid input', function (): void {
$action = createAction();
$action->handle(invalidData());
})->throws(InvalidArgumentException::class);
Mocking Dependencies
test('it calls dependency method', function (): void {
$mock = mock(EmailService::class);
$mock->shouldReceive('send')
->once()
->andReturn(true);
$action = new SendEmailAction($mock);
$result = $action->handle($data);
expect($result)->toBeTrue();
});
Helper Functions
function createAction(
?DependencyOne $depOne = null,
): MyAction {
return new MyAction(
dependencyOne: $depOne ?? new DependencyOne,
);
}
function createValidData(array $overrides = []): MyData
{
return MyData::from(array_merge([
'field1' => 'value1',
], $overrides));
}
Anti-Patterns
Don't Do This
// Don't skip return type
test('it can do something', function () {});
// Don't test multiple behaviors in one test
test('it can do everything', function (): void {
expect($service->methodOne())->toBeTrue();
expect($service->methodTwo())->toBe('value');
});
// Don't use database when unnecessary
test('it can calculate total', function (): void {
$order = Order::factory()->create(['subtotal' => 100]);
expect($order->calculateTotal())->toBe(110);
});
Do This Instead
// Always type hint return type
test('it can do something', function (): void {});
// One behavior per test
test('it can validate email format', function (): void {
expect($validator->isValidEmail('test@example.com'))->toBeTrue();
});
// Avoid database when possible
test('it can calculate total', function (): void {
$calculator = new OrderCalculator;
expect($calculator->calculateTotal(100, 0.1))->toBe(110);
});
Running Tests
php artisan test --testsuite=Unit
php artisan test tests/Unit/Support/Localisation/LocalisationTest.php
php artisan test --filter="it can get the ISO 15897 locale"
Quality Standards
- All unit tests must pass PHPStan level 8
- 100% type coverage required
- Tests must be independent and run in any order
- Prefer mocks/fakes over real dependencies
- Every public method should have unit tests
- Test edge cases, not just happy paths
References
- references/examples.md - Complete working examples
Score
Total Score
50/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon