Loading...
Loading...
Compare original and translation side by side
/\
/ \ Functional (10%)
/────\ - E2E, browser tests
/ \ - Slow, fragile
/────────\ Integration (20%)
/ \ - DB, HTTP, queues
/────────────\Unit (70%)
/ \- Fast, isolated
/________________\- Business logic /\
/ \ 功能测试 (10%)
/────\ - 端到端(E2E)、浏览器测试
/ \ - 速度慢、易失效
/────────\ 集成测试 (20%)
/ \ - 数据库、HTTP、队列相关
/────────────\单元测试 (70%)
/ \- 速度快、隔离性强
/________________\- 业务逻辑验证public function test_order_calculates_total_with_discount(): void
{
// Arrange — set up test data
$order = new Order(OrderId::generate());
$order->addItem(new Product('Book', Money::EUR(100)));
$discount = new PercentageDiscount(10);
// Act — execute the behavior
$total = $order->calculateTotal($discount);
// Assert — verify the outcome
self::assertEquals(Money::EUR(90), $total);
}public function test_order_calculates_total_with_discount(): void
{
// Arrange — 设置测试数据
$order = new Order(OrderId::generate());
$order->addItem(new Product('Book', Money::EUR(100)));
$discount = new PercentageDiscount(10);
// Act — 执行目标行为
$total = $order->calculateTotal($discount);
// Assert — 验证结果
self::assertEquals(Money::EUR(90), $total);
}test_{method}_{scenario}_{expected}| Example | Method | Scenario | Expected |
|---|---|---|---|
| calculateTotal | with discount | returns reduced amount |
| confirm | when already shipped | throws exception |
| Email (VO) | with invalid format | fails validation |
test_{方法名}_{场景}_{预期结果}| 示例 | 方法 | 场景 | 预期结果 |
|---|---|---|---|
| calculateTotal | 应用折扣 | 返回减免后的金额 |
| confirm | 订单已发货时 | 抛出异常 |
| Email(值对象) | 格式无效时 | 验证失败 |
it('calculates total with discount applied')
it('throws exception when confirming shipped order')
it('fails validation for invalid email format')it('calculates total with discount applied')
it('throws exception when confirming shipped order')
it('fails validation for invalid email format')| Rule | Check |
|---|---|
| One test = one behavior | Single assertion group |
| Test is documentation | Name reads as specification |
| No logic in tests | No if/for/while |
| Fast execution | <100ms per unit test |
| Mock interfaces only | Never mock VO, Entity, final |
| ≤3 mocks per test | More = design smell |
| Behavior over implementation | Test WHAT, not HOW |
| 规则 | 检查项 |
|---|---|
| 一个测试对应一个行为 | 仅包含一组断言 |
| 测试即文档 | 名称可作为规格说明阅读 |
| 测试中无业务逻辑 | 不包含if/for/while等逻辑 |
| 执行速度快 | 单元测试耗时<100ms |
| 仅模拟接口 | 绝不模拟值对象、实体、final类 |
| 每个测试最多3个模拟对象 | 超过3个意味着设计可能存在问题 |
| 优先验证行为而非实现 | 测试“做什么”,而非“怎么做” |
| Component | Test Focus | Mocks Allowed |
|---|---|---|
| Value Object | Validation, equality, immutability | None |
| Entity | State transitions, business rules | None |
| Aggregate | Invariants, consistency, events | None |
| Domain Service | Business logic spanning aggregates | Repository (Fake) |
| Application Service | Orchestration, transactions | Repository, EventDispatcher |
| Repository | CRUD operations | Database (SQLite) |
| 组件 | 测试重点 | 允许使用的模拟对象 |
|---|---|---|
| 值对象(Value Object) | 验证、相等性、不可变性 | 无 |
| 实体(Entity) | 状态转换、业务规则 | 无 |
| 聚合根(Aggregate) | 不变量、一致性、事件 | 无 |
| 领域服务(Domain Service) | 跨聚合根的业务逻辑 | 仓库(Fake实现) |
| 应用服务(Application Service) | 编排、事务 | 仓库、事件分发器 |
| 仓库(Repository) | CRUD操作 | 数据库(SQLite) |
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[Group('unit')]
#[CoversClass(Email::class)]
final class EmailTest extends TestCase
{
public function test_creates_valid_email(): void
{
$email = new Email('user@example.com');
self::assertSame('user@example.com', $email->value);
}
public function test_throws_for_invalid_format(): void
{
$this->expectException(InvalidArgumentException::class);
new Email('invalid');
}
}<?php
declare(strict_types=1);
namespace Tests\Unit\Domain;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[Group('unit')]
#[CoversClass(Email::class)]
final class EmailTest extends TestCase
{
public function test_creates_valid_email(): void
{
$email = new Email('user@example.com');
self::assertSame('user@example.com', $email->value);
}
public function test_throws_for_invalid_format(): void
{
$this->expectException(InvalidArgumentException::class);
new Email('invalid');
}
}<?php
declare(strict_types=1);
namespace Tests\Integration\Infrastructure;
use PHPUnit\Framework\Attributes\Group;
use Tests\DatabaseTestCase;
#[Group('integration')]
final class DoctrineOrderRepositoryTest extends DatabaseTestCase
{
private OrderRepositoryInterface $repository;
protected function setUp(): void
{
parent::setUp();
$this->repository = $this->getContainer()->get(OrderRepositoryInterface::class);
}
public function test_saves_and_retrieves_order(): void
{
// Arrange
$order = OrderMother::pending();
// Act
$this->repository->save($order);
$found = $this->repository->findById($order->id());
// Assert
self::assertNotNull($found);
self::assertTrue($order->id()->equals($found->id()));
}
}<?php
declare(strict_types=1);
namespace Tests\Integration\Infrastructure;
use PHPUnit\Framework\Attributes\Group;
use Tests\DatabaseTestCase;
#[Group('integration')]
final class DoctrineOrderRepositoryTest extends DatabaseTestCase
{
private OrderRepositoryInterface $repository;
protected function setUp(): void
{
parent::setUp();
$this->repository = $this->getContainer()->get(OrderRepositoryInterface::class);
}
public function test_saves_and_retrieves_order(): void
{
// Arrange
$order = OrderMother::pending();
// Act
$this->repository->save($order);
$found = $this->repository->findById($order->id());
// Assert
self::assertNotNull($found);
self::assertTrue($order->id()->equals($found->id()));
}
}| Type | Purpose | When to Use |
|---|---|---|
| Stub | Returns canned answers | External API responses |
| Mock | Verifies interactions | Event publishing |
| Fake | Working implementation | InMemory repository |
| Spy | Records calls | Logging, notifications |
| 类型 | 用途 | 使用场景 |
|---|---|---|
| Stub(桩) | 返回预设结果 | 模拟外部API响应 |
| Mock(模拟对象) | 验证交互行为 | 事件发布验证 |
| Fake(伪实现) | 可用的简化实现 | 内存仓库 |
| Spy(间谍) | 记录调用情况 | 日志、通知验证 |
Need to verify a call was made?
├── Yes → Mock or Spy
└── No → Need real behavior?
├── Yes → Fake
└── No → Stub需要验证是否调用了某个方法?
├── 是 → 使用Mock或Spy
└── 否 → 是否需要真实行为?
├── 是 → 使用Fake
└── 否 → 使用Stub| Smell | Detection | Fix |
|---|---|---|
| Logic in Test | | Extract to helper or parameterize |
| Mock Overuse | >3 mocks | Refactor design, use Fakes |
| Mystery Guest | External files, hidden data | Inline test data or use Builder |
| Eager Test | Tests multiple behaviors | Split into separate tests |
| Fragile Test | Breaks on refactor | Test behavior, not implementation |
| 坏味道 | 识别方式 | 修复方案 |
|---|---|---|
| 测试中包含业务逻辑 | 测试代码里有 | 提取到辅助方法或参数化测试 |
| 过度使用模拟对象 | 模拟对象数量>3 | 重构设计,改用Fake |
| 神秘访客 | 依赖外部文件、隐藏数据 | 内联测试数据或使用Builder模式 |
| 过度测试 | 一个测试验证多个行为 | 拆分为多个独立测试 |
| 脆弱测试 | 重构后容易失效 | 验证行为而非实现细节 |
references/unit-testing.mdreferences/integration-testing.mdreferences/ddd-testing.mdreferences/unit-testing.mdreferences/integration-testing.mdreferences/ddd-testing.md