name: acc-create-unit-test description: Generates PHPUnit unit tests for PHP 8.5. Creates isolated tests with AAA pattern, proper naming, attributes, and one behavior per test. Supports Value Objects, Entities, Services.
Unit Test Generator
Generates PHPUnit 11+ unit tests for PHP 8.5 classes.
Characteristics
- Isolated — no external dependencies (DB, HTTP, filesystem)
- Fast — executes in <100ms
- Focused — one behavior per test
- AAA Pattern — Arrange-Act-Assert structure
- Self-documenting — descriptive test names
Template
<?php
declare(strict_types=1);
namespace Tests\Unit\{Namespace};
use {FullyQualifiedClassName};
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[Group('unit')]
#[CoversClass({ClassName}::class)]
final class {ClassName}Test extends TestCase
{
private {ClassName} $sut;
protected function setUp(): void
{
$this->sut = new {ClassName}(/* dependencies */);
}
public function test_{method}_{scenario}_{expected}(): void
{
// Arrange
{arrange_code}
// Act
{act_code}
// Assert
{assert_code}
}
}
Naming Convention
test_{method}_{scenario}_{expected}
| Part | Description | Example |
|---|---|---|
{method} | Method under test | calculate_total |
{scenario} | Input condition | with_discount |
{expected} | Expected outcome | returns_reduced_price |
Examples:
test_confirm_when_pending_changes_status_to_confirmed
test_create_with_invalid_email_throws_exception
test_equals_with_same_value_returns_true
test_add_item_increases_total
Test Patterns by Component
Value Object Tests
#[Group('unit')]
#[CoversClass(Email::class)]
final class EmailTest extends TestCase
{
// Creation - valid
public function test_creates_with_valid_email(): void
{
$email = new Email('user@example.com');
self::assertSame('user@example.com', $email->value);
}
// Validation - invalid
public function test_throws_for_empty_value(): void
{
$this->expectException(InvalidArgumentException::class);
new Email('');
}
public function test_throws_for_invalid_format(): void
{
$this->expectException(InvalidArgumentException::class);
new Email('not-an-email');
}
// Equality
public function test_equals_returns_true_for_same_value(): void
{
$email1 = new Email('user@example.com');
$email2 = new Email('user@example.com');
self::assertTrue($email1->equals($email2));
}
public function test_equals_returns_false_for_different_value(): void
{
$email1 = new Email('user@example.com');
$email2 = new Email('other@example.com');
self::assertFalse($email1->equals($email2));
}
}
Entity Tests
#[Group('unit')]
#[CoversClass(Order::class)]
final class OrderTest extends TestCase
{
private Order $order;
protected function setUp(): void
{
$this->order = new Order(
OrderId::fromString('order-123'),
CustomerId::fromString('customer-456')
);
}
// Identity
public function test_has_unique_identity(): void
{
self::assertSame('order-123', $this->order->id()->toString());
}
// Initial state
public function test_is_pending_when_created(): void
{
self::assertTrue($this->order->isPending());
}
// State transitions - valid
public function test_confirm_changes_status_to_confirmed(): void
{
$this->order->addItem(ProductMother::book(), 1);
$this->order->confirm();
self::assertTrue($this->order->isConfirmed());
}
// State transitions - invalid
public function test_confirm_throws_when_already_confirmed(): void
{
$this->order->addItem(ProductMother::book(), 1);
$this->order->confirm();
$this->expectException(DomainException::class);
$this->order->confirm();
}
// Business rules
public function test_add_item_increases_total(): void
{
$this->order->addItem(ProductMother::withPrice(Money::EUR(100)), 2);
self::assertEquals(Money::EUR(200), $this->order->total());
}
// Domain events
public function test_records_order_confirmed_event(): void
{
$this->order->addItem(ProductMother::book(), 1);
$this->order->confirm();
$events = $this->order->releaseEvents();
self::assertCount(1, $events);
self::assertInstanceOf(OrderConfirmedEvent::class, $events[0]);
}
}
Domain Service Tests
#[Group('unit')]
#[CoversClass(TransferMoneyService::class)]
final class TransferMoneyServiceTest extends TestCase
{
private TransferMoneyService $service;
private InMemoryAccountRepository $repository;
private CollectingEventDispatcher $dispatcher;
protected function setUp(): void
{
$this->repository = new InMemoryAccountRepository();
$this->dispatcher = new CollectingEventDispatcher();
$this->service = new TransferMoneyService(
$this->repository,
$this->dispatcher
);
}
public function test_transfers_money_between_accounts(): void
{
// Arrange
$source = AccountMother::withBalance(Money::EUR(1000));
$target = AccountMother::withBalance(Money::EUR(500));
$this->repository->save($source);
$this->repository->save($target);
// Act
$this->service->transfer(
$source->id(),
$target->id(),
Money::EUR(300)
);
// Assert
$updatedSource = $this->repository->findById($source->id());
$updatedTarget = $this->repository->findById($target->id());
self::assertEquals(Money::EUR(700), $updatedSource->balance());
self::assertEquals(Money::EUR(800), $updatedTarget->balance());
}
public function test_throws_for_insufficient_funds(): void
{
// Arrange
$source = AccountMother::withBalance(Money::EUR(100));
$target = AccountMother::withBalance(Money::EUR(500));
$this->repository->save($source);
$this->repository->save($target);
// Assert
$this->expectException(InsufficientFundsException::class);
// Act
$this->service->transfer(
$source->id(),
$target->id(),
Money::EUR(300)
);
}
}
Data Providers
use PHPUnit\Framework\Attributes\DataProvider;
#[DataProvider('validEmailsProvider')]
public function test_accepts_valid_formats(string $email): void
{
$vo = new Email($email);
self::assertSame($email, $vo->value);
}
public static function validEmailsProvider(): array
{
return [
'simple' => ['user@example.com'],
'with subdomain' => ['user@mail.example.com'],
'with plus' => ['user+tag@example.com'],
'with dots' => ['first.last@example.com'],
];
}
#[DataProvider('invalidEmailsProvider')]
public function test_rejects_invalid_formats(string $email): void
{
$this->expectException(InvalidArgumentException::class);
new Email($email);
}
public static function invalidEmailsProvider(): array
{
return [
'empty' => [''],
'no at' => ['userexample.com'],
'no domain' => ['user@'],
'spaces' => ['user @example.com'],
];
}
Generation Instructions
-
Analyze the class:
- Identify public methods
- Identify dependencies (constructor parameters)
- Identify value objects (final readonly)
- Identify entities (has id, state changes)
- Identify services (orchestrates, uses repositories)
-
Determine test cases:
- Happy path for each method
- Edge cases (null, empty, boundary)
- Exception paths (validation failures)
- State transitions (for entities)
-
Generate test class:
- Match namespace:
src/Domain/Order/Order.php→tests/Unit/Domain/Order/OrderTest.php - Add attributes:
#[Group('unit')],#[CoversClass] - Create setUp if shared state needed
- Match namespace:
-
Generate test methods:
- Follow naming convention
- Use AAA structure
- One assertion group per test
-
Add helpers if needed:
- Use existing Mothers/Builders
- Create inline builders for simple cases
Assertions Reference
// Value comparisons
self::assertSame($expected, $actual); // ===
self::assertEquals($expected, $actual); // ==
self::assertTrue($condition);
self::assertFalse($condition);
self::assertNull($value);
self::assertNotNull($value);
// Types
self::assertInstanceOf(ClassName::class, $object);
// Strings
self::assertStringContainsString($needle, $haystack);
self::assertStringStartsWith($prefix, $string);
// Arrays
self::assertCount($expected, $array);
self::assertContains($needle, $array);
self::assertArrayHasKey($key, $array);
// Exceptions
$this->expectException(ExceptionClass::class);
$this->expectExceptionMessage('message');
$this->expectExceptionCode(404);
Usage
Provide:
- Path to class to test
- Or class name and namespace
- Specific methods to focus on (optional)
The generator will:
- Read the source class
- Analyze methods and dependencies
- Generate comprehensive test class
- Include happy path + edge cases + exceptions