在这个时代,编写单元测试的好处是巨大的。我认为大多数最近启动的项目都包含单元测试。在具有大量业务逻辑的企业应用程序中,单元测试是最重要的测试,因为它们速度很快并且可以让我们立即确保我们的实现是正确的。然而,我经常在项目中看到良好测试的问题,尽管只有当您拥有良好的单元测试时,这些测试的好处才会巨大。因此,在这些示例中,我将尝试分享一些有关如何编写良好的单元测试的技巧。
易于阅读的版本: https://testing-tips.sarvendev.com/
卡米尔·鲁钦斯基
博客: https://sarvendev.com/
领英: https://www.linkedin.com/in/kamilruczynski/
您的支持对我来说意味着整个世界!如果您喜欢本指南并发现共享知识的价值,请考虑在 BuyMeCoffee 上支持我:
或者只是在存储库上留下一个星号,然后在 Twitter 和 Github 上关注我,以了解所有更新。您的慷慨激起了我为您创造更具洞察力的内容的热情。
如果您有任何改进想法或要写的主题,请随时准备拉取请求或直接告诉我。
订阅我的免费电子书并掌握单元测试!
细节
我仍然有一个很长的待办事项列表,其中列出了本关于单元测试的指南的改进,我将在不久的将来介绍它们。
介绍
作者
测试双打
命名
AAA图案
对象母亲
建设者
断言对象
参数化测试
单元测试的两个流派
古典
模仿者
依赖关系
模拟与存根
单元测试的三种风格
输出
状态
沟通
功能架构和测试
可观察的行为与实现细节
行为单位
谦卑的格局
简单的测试
易碎测试
测试治具
一般测试反模式
暴露私人国家
泄露域名详细信息
模拟具体类
测试私有方法
时间作为不稳定的依赖项
100% 测试覆盖率不应该是目标
推荐书籍
测试替身是测试中使用的虚假依赖项。
虚拟对象只是一个简单的实现,不执行任何操作。
最终类 Mailer 实现 MailerInterface {公共函数发送(消息$消息):无效{ } }
假货是模拟原始行为的简化实现。
最终类 InMemoryCustomerRepository 实现 CustomerRepositoryInterface {/** * @var Customer[] */私有数组 $customers;公共函数 __construct() {$this->客户=[]; }public function store(Customer $customer): void{$this->customers[(string) $customer->id()->id()] = $customer; }公共函数 get(CustomerId $id): Customer{if (!isset($this->customers[(string) $id->id()])) {抛出 new CustomerNotFoundException(); }return $this->customers[(string) $id->id()]; }public function findByEmail(Email $email): Customer{foreach ($this->customers as $customer) {if ($customer->getEmail()->isEqual($email)) {return $customer; } }抛出新的CustomerNotFoundException(); } }
存根是具有硬编码行为的最简单实现。
最终类 UniqueEmailSpecificationStub 实现 UniqueEmailSpecificationInterface {公共函数 isUnique(电子邮件 $email): bool{return true; } }
$specationStub = $this->createStub(UniqueEmailSpecificationInterface::class);$specationStub->method('isUnique')->willReturn(true);
间谍是验证特定行为的实现。
最终类 Mailer 实现 MailerInterface {/** * @var Message[] */私有数组$messages; 公共函数 __construct() {$this->消息=[]; }公共函数send(消息$message): void{$this->messages[] = $message; }公共函数 getCountOfSentMessages(): int{return count($this->messages); } }
模拟是一种配置的模仿,用于验证对协作者的调用。
$message = new Message('[email protected]', '测试', '测试测试测试');$mailer = $this->createMock(MailerInterface::class);$mailer->expects($this->一次()) ->方法('发送') ->with($this->equalTo($message));
[!注意] 要验证传入的交互,请使用存根,但要验证传出的交互,请使用模拟。
更多:模拟与存根
[!警告|样式:平面|标签:不好]
最终类 TestExample 扩展了 TestCase {/** * @test */公共函数send_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->method('getAll')->willReturn([$message1, $message2]);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('send') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!提示|样式:扁平|标签:更好]
更好地抵抗重构
对特定方法使用 Refactor->Rename 不会破坏测试
更好的可读性
降低可维护成本
不需要学习那些复杂的mock框架
只需简单的纯 PHP 代码
最终类 TestExample 扩展了 TestCase {/** * @test */公共函数send_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$mailer = new SpyMailer();$sut = 新的NotificationService($mailer, $messageRepository);$sut->send(); $mailer->assertThatMessagesHaveBeenSent([$message1, $message2]); } }
[!警告|样式:平面|标签:不好]
公共函数测试(): void{$subscription = SubscriptionMother::new();$subscription->activate();self::assertSame(Status::activated(), $subscription->status()); }
[!TIP|style:flat|label:明确指定您正在测试的内容]
public function sut(): void{// sut = 测试中的系统$sut = SubscriptionMother::new();$sut->activate();self::assertSame(Status::activated(), $sut->status ()); }
[!警告|样式:平面|标签:不好]
公共函数 it_throws_invalid_credentials_exception_when_sign_in_with_invalid_credentials(): void{ }公共函数 testCreatingWithATooShortPasswordIsNotPossible(): void{ }公共函数 testDeactivateASubscription(): void{ }
[!提示|样式:扁平|标签:更好]
使用下划线提高可读性
名称应该描述行为,而不是实现
使用没有技术关键字的名称。它对于非程序员来说应该是可读的。
公共函数sign_in_with_invalid_credentials_is_not_possible():void { }公共函数create_with_a_too_short_password_is_not_possible(): void{ }公共函数 deactivating_an_activated_subscription_is_valid(): void{ }公共函数 deactivating_an_inactive_subscription_is_invalid(): void{ }
笔记
描述行为对于测试领域场景非常重要。如果您的代码只是一个实用程序,那么它就不那么重要了。
为什么非程序员阅读单元测试会有用?
如果有一个项目的领域逻辑很复杂,那么这个逻辑对于每个人来说都必须非常清晰,所以然后测试描述领域细节,而不需要技术关键字,你可以用这些测试中的语言与业务进行对话。所有与域相关的代码都应该不含技术细节。非程序员不会阅读这些测试。如果您想谈论该域,这些测试将有助于了解该域的用途。会有一些没有技术细节的描述,例如返回null、抛出异常等。此类信息与领域无关,因此我们不应该使用这些关键字。
也很常见的是“Given”、“When”、“Then”。
测试分为三个部分:
安排:使受测系统处于所需状态。准备依赖项、参数并最终构建 SUT。
Act :调用经过测试的元素。
Assert :验证结果、最终状态或与协作者的沟通。
[!提示|样式:平面|标签:好]
public function aaa_pattern_example_test(): void{//Arrange|Given$sut = SubscriptionMother::new();//Act|When$sut->activate();//Assert|Thenself::assertSame(Status::activated( ), $sut->status()); }
该模式有助于创建可以在一些测试中重用的特定对象。因此,安排部分很简洁,并且整个测试更具可读性。
最后一堂订阅妈妈课 {public static function new(): 订阅{return new Subscription(); }公共静态函数激活():订阅{$订阅=新订阅();$订阅->激活();返回$订阅; }public static function deactivated(): Subscription{$subscription = self::activated();$subscription->deactivate();return $subscription; } }
最终类示例测试 {public function example_test_with_activated_subscription(): void{$activatedSubscription = SubscriptionMother::activated();// 做某事// 检查某事}public function example_test_with_deactivated_subscription(): void{$deactivatedSubscription = SubscriptionMother::deactivated();// 做某事// 检查一些东西} }
Builder 是另一种帮助我们在测试中创建对象的模式。与对象母体模式生成器相比,它更适合创建更复杂的对象。
最终类 OrderBuilder {private DateTimeImmutable|null $createdAt = null;/** * @var OrderItem[] */私有数组 $items = [];public function createdAt(DateTimeImmutable $createdAt): self{$this->createdAt = $createdAt;return $这个; }public function withItem(string $name, int $price): self{$this->items[] = new OrderItem($name, $price);return $this; }公共函数 build(): 订单{ 断言::notEmpty($this->items);返回新订单($this->createdAt ?? new DateTimeImmutable(),$this->items, ); } }
最终类ExampleTest扩展了TestCase {/** * @test */公共函数 example_test_with_order_builder(): void{$order = (new OrderBuilder()) ->createdAt(new DateTimeImmutable('2022-11-10 20:00:00')) ->withItem('项目 1', 1000) ->withItem('项目 2', 2000) ->withItem('项目 3', 3000) ->build();// 做某事// 检查某事} }
断言对象模式有助于编写更具可读性的断言部分。我们可以准备一个抽象,并使用自然语言来描述预期的结果,而不是使用一些断言。
最终类ExampleTest扩展了TestCase {/** * @test */public function example_test_with_asserter(): void{$currentTime = new DateTimeImmutable('2022-11-10 20:00:00');$sut = new OrderService();$order = $sut ->创建($当前时间); OrderAsserter::assertThat($order) ->wasCreatedAt($currentTime) -> 总数量(6000); } }
使用 PHPUnitFrameworkAssert;最终类 OrderAsserter {public function __construct(private readonly Order $order) {}public static function assertThat(Order $order): self{return new OrderAsserter($order); }公共函数 wasCreatedAt(DateTimeImmutable $createdAt): self{ 断言::assertEquals($createdAt, $this->order->createdAt);return $this; }公共函数 hasTotal(int $total): self{ 断言::assertSame($total, $this->order->getTotal());return $this; } }
参数化测试是一个很好的选择,可以使用许多参数来测试 SUT,而无需重复代码。
警告
这种测试的可读性较差。为了增加一点可读性,负面和正面的例子应该分成不同的测试。
最终类ExampleTest扩展了TestCase {/** * @test * @dataProvider getInvalidEmails */public function detectors_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( $结果); }/** * @test * @dataProvider getValidEmails */public function detectors_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( $结果); }public function getInvalidEmails(): iterable{yield '一封没有@的无效电子邮件' => ['test'];yield '@后没有域名的无效电子邮件' => ['test@'];yield '一封无效电子邮件without TLD' => ['test@test'];//...}public function getValidEmails(): iterable{yield '带有小写字母的有效电子邮件' => ['[email protected]'];yield '包含小写字母和数字的有效电子邮件' => ['[email protected]'];yield '包含大写字母和数字的有效电子邮件' => ['Test123@ test.com'];//...} }
笔记
使用yield
并为case添加文本描述以提高可读性。
该单元是单个行为单元,它可以是几个相关的类。
每个测试都应该与其他测试隔离。因此必须可以并行或以任何顺序调用它们。
最终类 TestExample 扩展了 TestCase {/** * @test */公共函数 suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = new CanAlwaysSuspendPolicy();$sut = new Subscription();$result = $sut->挂起($canAlwaysSuspendPolicy);self::assertTrue($result);self::assertSame(状态::挂起(), $sut->status()); } }
该单元是一个班级。
该单位应与所有合作者隔离。
最终类 TestExample 扩展了 TestCase {/** * @test */public 函数 suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = $this->createStub(SuspendingPolicyInterface::class);$canAlwaysSuspendPolicy->method('suspend')->willReturn(true);$ sut = 新订阅();$结果= $sut->挂起($canAlwaysSuspendPolicy);self::assertTrue($result);self::assertSame(状态::挂起(), $sut->status()); } }
笔记
经典方法最好避免脆弱的测试。
[待办事项]
例子:
最终类通知服务 {public function __construct(private readonly MailerInterface $mailer,private readonly MessageRepositoryInterface $messageRepository) {}public function send(): void{$messages = $this->messageRepository->getAll();foreach ($messages as $message) { $this->邮件程序->发送($message); } } }
[!警告|样式:平面|标签:不好]
断言与存根的交互会导致脆弱的测试
最终类 TestExample 扩展了 TestCase {/** * @test */公共函数send_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->method('getAll')->willReturn([$message1, $message2]);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$messageRepository->expects(self::once())->method('getAll');$mailer- >期望(self::exactly(2))->方法('发送') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!提示|样式:平面|标签:好]
最终类 TestExample 扩展了 TestCase {/** * @test */公共函数send_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->保存($message2);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);// 删除了与存根的断言交互$mailer->expects(self::exactly(2))->方法('发送') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|style:flat|label:使用 SPY 效果更好]
最终类 TestExample 扩展了 TestCase {/** * @test */公共函数send_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$mailer = new SpyMailer();$sut = 新的NotificationService($mailer, $messageRepository);$sut->send(); $mailer->assertThatMessagesHaveBeenSent([$message1, $message2]); } }
[!TIP|样式:扁平|标签:最佳选择]
最好的重构抵抗力
最佳准确度
最低的可维护性成本
如果可能的话,你应该更喜欢这种测试
最终类ExampleTest扩展了TestCase {/** * @test * @dataProvider getInvalidEmails */public function detectors_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( $结果); }/** * @test * @dataProvider getValidEmails */public function detectors_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( $结果); }公共函数 getInvalidEmails(): 数组{return [ ['测试'], ['测试@'], ['测试@测试'],//...]; }公共函数 getValidEmails(): 数组{return [ ['[email protected]'], ['[email protected]'], ['[email protected]'],//...]; } }
[!警告|样式:扁平|标签:更糟糕的选择]
更糟糕的重构阻力
准确度较差
可维护性成本较高
最终类ExampleTest扩展了TestCase {/** * @test */公共函数adding_an_item_to_cart(): void{$item = new CartItem('Product');$sut = new Cart();$sut->addItem($item);self::assertSame (1, $sut->getCount());self::assertSame($item, $sut->getItems()[0]); } }
[!注意|风格:扁平|标签:最糟糕的选择]
重构的最大阻力
最差的准确度
可维护性成本最高
最终类ExampleTest扩展了TestCase {/** * @test */公共函数send_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->保存($message2);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('send') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!警告|样式:平面|标签:不好]
最终类NameService {公共函数 __construct(私有只读 CacheStorageInterface $cacheStorage) {}公共函数 loadAll(): void{$namesCsv = array_map('str_getcsv', file(__DIR__.'/../names.csv'));$names = [ ];foreach ($namesCsv as $nameData) {if (!isset($nameData[0], $nameData[1])) {继续; }$names[] = 新姓名($nameData[0], 新性别($nameData[1])); }$this->cacheStorage->store('names', $names); } }
如何测试这样的代码?这只能通过集成测试来实现,因为它直接使用与文件系统相关的基础设施代码。
[!提示|样式:平面|标签:好]
就像在函数式架构中一样,我们需要将具有副作用的代码和仅包含逻辑的代码分开。
最终类NameParser {/** * @param array<string[]> $namesData * @return Name[] */public function parse(array $namesData): array{$names = [];foreach ($namesData as $nameData) {if (!isset($nameData[0], $nameData[1])) {继续; }$names[] = 新姓名($nameData[0], 新性别($nameData[1])); }返回$名称; } }
最终类 CsvNamesFileLoader {公共函数load(): array{return array_map('str_getcsv', file(__DIR__.'/../names.csv')); } }
最终类ApplicationService {public function __construct(private readonly CsvNamesFileLoader $fileLoader,private readonly NameParser $parser,private readonly CacheStorageInterface $cacheStorage) {}public function loadNames(): void{$namesData = $this->fileLoader->load();$names = $this->解析器->parse($namesData);$this->cacheStorage->store('names', $names); } }
最终类 ValidUnitExampleTest 扩展了 TestCase {/** * @test */public function parse_all_names(): void{$namesData = [ ['约翰','M'], ['列侬','U'], ['莎拉','W'] ];$sut = new NameParser();$result = $sut->parse($namesData); 自我::断言相同( [新姓名('约翰',新性别('M')),新姓名('列侬',新性别('U')),新姓名('莎拉',新性别('W')) ],$结果); } }
[!警告|样式:平面|标签:不好]
最终类ApplicationService {public function __construct(private readonly SubscriptionRepositoryInterface $subscriptionRepository) {}public function renewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);if (!$subscription->getStatus() ->isEqual(Status::expired())) {返回 false; }$subscription->setStatus(Status::active());$subscription->setModifiedAt(new DateTimeImmutable());返回 true; } }
最后一堂课订阅 {public function __construct(private Status $status, private DateTimeImmutable $modifiedAt) {}public function getStatus(): Status{return $this->status; }公共函数 setStatus(状态 $status): void{$this->status = $status; }公共函数 getModifiedAt(): DateTimeImmutable{return $this->modifiedAt; }公共函数 setModifiedAt(DateTimeImmutable $modifiedAt): void{$this->modifiedAt = $modifiedAt; } }
最终类 InvalidTestExample 扩展了 TestCase {/** * @test */public 函数 renew_an_expired_subscription_is_possible(): void{$modifiedAt = new DateTimeImmutable();$expiredSubscription = new Subscription(Status::expired(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($expiredSubscription));$结果 = $sut->renewSubscription(1);self::assertSame(Status::active(), $expiredSubscription->getStatus());self::assertGreaterThan($modifiedAt, $expiredSubscription->getModifiedAt());self::断言True($结果); }/** * @test */public 函数 renew_an_active_subscription_is_not_possible(): void{$modifiedAt = new DateTimeImmutable();$activeSubscription = new Subscription(Status::active(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($activeSubscription));$结果 = $sut->renewSubscription(1);self::assertSame($modifiedAt, $activeSubscription->getModifiedAt());self::assertFalse($result); } private function createRepository(Subscription $subscription): SubscriptionRepositoryInterface{返回新类 ($expiredSubscription) 实现 SubscriptionRepositoryInterface {public function __construct(private readonly Subscription $subscription) {} 公共函数 findById(int $id): 订阅{return $this->订阅; } }; } }
[!提示|样式:平面|标签:好]
最终类ApplicationService {public function __construct(private readonly SubscriptionRepositoryInterface $subscriptionRepository) {}public function renewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);return $subscription->renew(new DateTimeImmutable( )); } }
最后一堂课订阅 {私有状态$status;私有DateTimeImmutable $modifiedAt;公共函数__construct(DateTimeImmutable $modifiedAt) {$this->status = Status::new();$this->modifiedAt = $modifiedAt; }public function renew(DateTimeImmutable $modifiedAt): bool{if (!$this->status->isEqual(Status::expired())) {return false; } }$this->status = Status::active();$this->modifiedAt = $modifiedAt;返回 true; }public function active(DateTimeImmutable $modifiedAt): void{//简化$this->status = Status::active();$this->modifiedAt = $modifiedAt; }public function expire(DateTimeImmutable $modifiedAt): void{//简化$this->status = Status::expired();$this->modifiedAt = $modifiedAt; }公共函数 isActive(): bool{return $this->status->isEqual(Status::active()); } }
最终类 ValidTestExample 扩展了 TestCase {/** * @test */public function renew_an_expired_subscription_is_possible(): void{$expiredSubscription = SubscriptionMother::expired();$sut = new ApplicationService($this->createRepository($expiredSubscription));$result = $sut- >renewSubscription(1);// 跳过检查modifiedAt,因为它不是可观察行为的一部分。要检查这个值,我们//必须为modifiedAt添加一个getter,可能仅用于测试目的。self::assertTrue($expiredSubscription->isActive());self::assertTrue($result); }/** * @test */public 函数 renew_an_active_subscription_is_not_possible(): void{$activeSubscription = SubscriptionMother::active();$sut = new ApplicationService($this->createRepository($activeSubscription));$result = $sut->renewSubscription(1);self::assertTrue($activeSubscription->isActive());self::assertFalse($result); } private function createRepository(Subscription $subscription): SubscriptionRepositoryInterface{返回新类 ($expiredSubscription) 实现 SubscriptionRepositoryInterface {public function __construct(private readonly Subscription $subscription) {} 公共函数 findById(int $id): 订阅{return $this->订阅; } }; } }
笔记
第一个订阅模式的设计很糟糕。要调用一项业务操作,您需要调用三个方法。另外,使用 getter 来验证操作也不是一个好的做法。在这种情况下,它会跳过检查modifiedAt
的更改,可能在续订操作期间设置特定的modifiedAt
可以通过过期业务操作进行测试。 modifiedAt
的 getter 不是必需的。当然,在某些情况下,找到避免仅为测试提供 getter 的可能性会非常困难,但我们始终应该尽量不要引入它们。
[!警告|样式:平面|标签:不好]
类 CannotSuspendExpiredSubscriptionPolicy 实现 SuspendingPolicyInterface {公共函数挂起(订阅 $subscription, DateTimeImmutable $at): bool{if ($subscription->isExpired()) {返回 false; }返回真; } }
类 CannotSuspendExpiredSubscriptionPolicyTest 扩展了 TestCase {/** * @test */public function it_returns_false_when_a_subscription_is_expired(): void{$policy = new CannotSuspendExpiredSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isExpired') ->willReturn(true);self::assertFalse($policy->suspend($subscription,新的 DateTimeImmutable())); }/** * @test */公共函数 it_returns_true_when_a_subscription_is_not_expired(): void{$policy = new CannotSuspendExpiredSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isExpired')->willReturn(false);self::assertTrue($policy->suspend($subscription, new DateTimeImmutable())); } }
类 CannotSuspendNewSubscriptionPolicy 实现 SuspendingPolicyInterface {公共函数挂起(订阅 $subscription,DateTimeImmutable $at): bool{if ($subscription->isNew()) {返回 false; }返回真; } }
类 CannotSuspendNewSubscriptionPolicyTest 扩展 TestCase {/** * @test */public function it_returns_false_when_a_subscription_is_new(): void{$policy = new CannotSuspendNewSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isNew') ->willReturn(true);self::assertFalse($policy->suspend($subscription, new日期时间不可变())); }/** * @test */public function it_returns_true_when_a_subscription_is_not_new(): void{$policy = new CannotSuspendNewSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isNew') ->willReturn(false);self::assertTrue($policy->suspend($subscription, new日期时间不可变())); } }
类 CanSuspendAfterOneMonthPolicy 实现 SuspendingPolicyInterface {公共函数挂起(订阅 $subscription, DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
CanSuspendAfterOneMonthPolicyTest 类扩展了 TestCase {/** * @test */public 函数 it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable('2021-01-29');$policy = new CanSuspendAfterOneMonthPolicy();$subscription = new Subscription(new DateTimeImmutable('2020-12-28'));self::assertTrue($policy->suspend($subscription, $date)); }/** * @test */public 函数 it_returns_false_when_a_subscription_is_not_older_than_one_month(): void{$date = new DateTimeImmutable('2021-01-29');$policy = new CanSuspendAfterOneMonthPolicy();$subscription = new Subscription(new DateTimeImmutable('2020-01-01'));self::assertTrue($policy->suspend($subscription, $date)); } }
班级状态 {private const EXPIRED = '过期';private const ACTIVE = '活动';private const NEW = '新';private const SUSPENDED = '挂起';私有函数 __construct(private readonly string $status) {$this->状态=$状态; }公共静态函数expired(): self{返回新的self(self::EXPIRED); }public static function active(): self{返回新的 self(self::ACTIVE); }公共静态函数new(): self{返回新的self(self::NEW); }公共静态函数挂起(): self{返回新的self(self::SUSPENDED); }public function isEqual(self $status): bool{return $this->status === $status->status; } }
类 StatusTest 扩展 TestCase {公共函数 testEquals(): void{$status1 = Status::active();$status2 = Status::active();self::assertTrue($status1->isEqual($status2)); }公共函数 testNotEquals(): void{$status1 = Status::active();$status2 = Status::expired();self::assertFalse($status1->isEqual($status2)); } }
类 SubscriptionTest 扩展 TestCase {/** * @test */public 函数 suspending_a_subscription_is_possible_when_a_policy_returns_true(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( '挂起')->willReturn(true);$sut = new 订阅(new DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertTrue($result);self::assertTrue($sut->isSuspended()); }/** * @test */公共函数 suspending_a_subscription_is_not_possible_when_a_policy_returns_false(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( '挂起')->willReturn(false);$sut = new 订阅(new DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertFalse($result);self::assertFalse($sut->isSuspended()); }/** * @test */public function it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1M'));$sut = new Subscription($日期);self::assertTrue($sut->isOlderThan($futureDate)); }/** * @test */public 函数 it_returns_false_when_a_subscription_is_not_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1D'));$sut = new订阅($date);self::assertTrue($sut->isOlderThan($futureDate)); } }
[!注意]不要以 1:1、1 类 : 1 测试的方式编写代码。它导致脆弱的测试,使得重构变得困难。
[!提示|样式:平面|标签:好]
最终类 CannotSuspendExpiredSubscriptionPolicy 实现 SuspendingPolicyInterface {公共函数挂起(订阅 $subscription, DateTimeImmutable $at): bool{if ($subscription->isExpired()) {返回 false; }返回真; } }
最终类 CannotSuspendNewSubscriptionPolicy 实现 SuspendingPolicyInterface {公共函数挂起(订阅 $subscription,DateTimeImmutable $at): bool{if ($subscription->isNew()) {返回 false; }返回真; } }
最终类 CanSuspendAfterOneMonthPolicy 实现 SuspendingPolicyInterface {公共函数挂起(订阅 $subscription, DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
最后一堂课状态 {private const EXPIRED = '过期';private const ACTIVE = '活动';private const NEW = '新';private const SUSPENDED = '挂起';私有函数 __construct(private readonly string $status) {$this->状态=$状态; }公共静态函数expired(): self{返回新的self(self::EXPIRED); }public static function active(): self{返回新的 self(self::ACTIVE); }公共静态函数new(): self{返回新的self(self::NEW); }公共静态函数挂起(): self{返回新的self(self::SUSPENDED); }public function isEqual(self $status): bool{return $this->status === $status->status; } }
最后一堂课订阅 {私有状态$status;私有DateTimeImmutable $createdAt;公共函数__construct(DateTimeImmutable $createdAt) {$this->status = Status::new();$this->createdAt = $createdAt; }公共函数挂起(SuspendingPolicyInterface $ suspendingPolicy,DateTimeImmutable $ at):bool {$结果= $ suspendingPolicy - >挂起($ this,$ at); if($结果){$ this - >状态=状态::挂起() ; }返回$结果; }公共函数 isOlderThan(DateTimeImmutable $date): bool{return $this->createdAt < $date; }公共函数activate(): void{$this->status = Status::active(); }公共函数expire(): void{$this->status = Status::expired(); }公共函数 isExpired(): bool{return $this->status->isEqual(Status::expired()); }公共函数 isActive(): bool{return $this->status->isEqual(Status::active()); }公共函数 isNew(): bool{return $this->status->isEqual(Status::new()); }公共函数 isSuspended(): bool{return $this->status->isEqual(Status::suspended()); } }
最终类 SubscriptionSuspendingTest 扩展了 TestCase {/** * @test */公共函数 suspending_an_expired_subscription_with_cannot_suspend_expired_policy_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$sut->expire();$result = $sut -> 暂停(新 CannotSuspendExpiredSubscriptionPolicy(),新DateTimeImmutable());self::assertFalse($result); }/** * @test */公共函数 suspending_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable());$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy(), new DateTimeImmutable());self ::assertFalse($结果); }/** * @test */公共函数 suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy() , 新的DateTimeImmutable());self::assertTrue($结果); }/** * @test */公共函数 suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendExpiredSubscriptionPolicy() , 新的DateTimeImmutable());self::assertTrue($结果); }/** * @test */public function suspending_an_subscription_before_a_one_month_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable('2020-01-01'));$result = $sut->suspend(new CanSuspendAfterOneMonthPolicy(),新的DateTimeImmutable('2020-01-10'));self::assertFalse($结果); }/** * @test */public function suspending_an_subscription_after_a_one_month_is_possible(): void{$sut = new Subscription(new DateTimeImmutable('2020-01-01'));$result = $sut->suspend(new CanSuspendAfterOneMonthPolicy(),新的DateTimeImmutable('2020-02-02'));self::assertTrue($结果); } }
如何正确地对这样的类进行单元测试?
应用服务类 {public function __construct(private readonly OrderRepository $orderRepository,private readonly FormRepository $formRepository) {}public function changeFormStatus(int $orderId): void{$order = $this->orderRepository->getById($orderId);$soapResponse = $ this->getSoapClient()->getStatusByOrderId($orderId);$form = $this->formRepository->getByOrderId($orderId);$form->setStatus($soapResponse['status']);$form->setModifiedAt(new DateTimeImmutable());if ($soapResponse['status'] = == '已接受') {$order->setStatus('已付款'); }$this->formRepository->save($form);$this->orderRepository->save($order); }私有函数 getSoapClient(): SoapClient{return new SoapClient('https://legacy_system.pl/Soap/WebService', []); } }
[!提示|样式:平面|标签:好]
需要将过于复杂的代码拆分为单独的类。
最终类ApplicationService {公共函数__construct(私有只读OrderRepositoryInterface $orderRepository,私有只读FormRepositoryInterface $formRepository,私有只读FormApiInterface $formApi,私有只读ChangeFormStatusService $changeFormStatusService) {}公共函数changeFormStatus(int $orderId): void{$order = $this->orderRepository ->getById($orderId);$form = $this->formRepository->getByOrderId($orderId);$status = $this->formApi->getStatusByOrderId($orderId);$this->changeFormStatusService->changeStatus($order, $form, $status);$this ->formRepository->save($form);$this->orderRepository->save($order); } }
最终类 ChangeFormStatusService {公共函数changeStatus(订单$order,表单$form,字符串$formStatus): void{$status = FormStatus::createFromString($formStatus);$form->changeStatus($status);if ($form->isAccepted( )) {$order->changeStatus(OrderStatus::paid()); } } }
最终类ChangingFormStatusTest扩展了TestCase {/** * @test */公共函数changing_a_form_status_to_accepted_changes_an_order_status_to_paid(): void{$order = new Order();$form = new Form();$status = '已接受';$sut = new ChangeFormStatusService();$sut ->changeStatus($order, $form, $status);self::assertTrue($form->isAccepted());self::assertTrue($order->isPaid()); }/** * @test */公共函数changing_a_form_status_to_refused_not_changes_an_order_status(): void{$order = new Order();$form = new Form();$status = 'new';$sut = new ChangeFormStatusService();$sut ->changeStatus($order, $form, $status);self::assertFalse($form->isAccepted());self::assertFalse($order->isPaid()); } }
但是,ApplicationService 可能应该通过仅使用模拟的 FormApiInterface 进行集成测试来进行测试。
[!警告|样式:平面|标签:不好]
最后一堂课客户 {public function __construct(private string $name) {}public function getName(): string{return $this->name; }公共函数 setName(string $name): void{$this->name = $name; } }
最终类 CustomerTest 扩展了 TestCase {public function testSetName(): void{$customer = new Customer('Jack');$customer->setName('John');self::assertSame('John', $customer->getName()); } }
最终类 EventSubscriber {public static function getSubscribedEvents(): array{return ['event' => 'onEvent']; }公共函数 onEvent(): void{ } }
最终类 EventSubscriberTest 扩展了 TestCase {公共函数 testGetSubscribedEvents(): void{$result = EventSubscriber::getSubscribedEvents();self::assertSame(['event' => 'onEvent'], $result); } }
[!注意] 在没有任何复杂逻辑的情况下测试代码是没有意义的,而且还会导致脆弱的测试。
[!警告|样式:平面|标签:不好]
最终类 UserRepository {public function __construct(private readonly Connection $connection) {}public function getUserNameByEmail(string $email): ?array{return $this->connection->createQueryBuilder() ->来自('用户', 'u') ->where('u.email = :email') ->setParameter('email', $email) ->执行() ->获取(); } }
最终类 TestUserRepository 扩展了 TestCase {公共函数 testGetUserNameByEmail(): void{$email = '[email protected]';$connection = $this->createMock(Connection::class);$queryBuilder = $this->createMock(QueryBuilder::class); $result = $this->createMock(ResultStatement::class);$userRepository = new UserRepository($connection);$connection->expects($this->once()) ->方法('createQueryBuilder') ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->方法('来自') ->with('用户', 'u') ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->方法('哪里') ->with('u.email = :email') ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->方法('设置参数') ->with('电子邮件', $电子邮件) ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->方法('执行') ->willReturn($result);$result->expects($this->once()) - >方法('fetch') - > willreturn([['email'=> $ email]); $ result = $ userrepository-> getUsernameByemail($ email); self :: assertSame(['email'=> $ email],$ result); } }
[!注意] 以这种方式测试存储库会导致测试脆弱,然后重构就很困难。要测试存储库,请编写集成测试。
[!提示|样式:平面|标签:好]
最后一类Goodtest扩展了测试柜 {private subscriptionFactory $ sut; public function setup():void {$ this-> sut = new subscriptionFactory(); }/** * @test */public函数creates_a_subscription_for_a_a_given_date_range():void {$ result = $ this-> sut-> sut-> create(new dateTimeImmutable(),new dateTimeMmutable(),new dateTimeMmutable(new dateTimeMmutable() (订阅:: class,$ result); }/** * @test */public函数throws_an_exception_on_invalid_date_range():void {$ this-> expectexception(createSubScriptionException :: class); $ result = $ this-> sut-> create(new dateTimeImmutable('Now -1年'),new DateTimeImmutable()); } }
笔记
使用 setUp 方法的最佳情况是测试无状态对象。
setUp
中进行的任何配置都会将测试结合在一起,并对所有测试产生影响。
最好避免测试之间共享状态并根据测试方法配置初始状态。
与通过正确的测试方法进行的配置相比,可读性较差。
[!提示|样式:扁平|标签:更好]
最后一类更好的测试扩展了测试柜 {/** * @test */public函数悬浮_an_active_subscription_with_with_cannot_suspend_new_new_new_policy_is_possible():void {$ sut = $ sut = $ sut-> createAnActivesUbscription() :asserttrue($ result); }/** * @test */公共功能暂停_an_active_subscription_with_cannot_suspend_suspend_expired_expired_policy_is_possible():void {$ sut = $ sut = $ sut = $ the-> createAnActivesUbscription() :asserttrue($ result); }/** * @test */public函数悬浮_a_new_subscription_with_cannot_suspend_new_new_new_policy_is_is_not_possible():void {$ sut = $ sut = $ thiseanewseanewsubscription() :assertfalse($ result); }私有函数createAnewSubscription():subscription {返回新订阅(new DateTimeImmutable()); } private函数createAnactiveUbscription():subscription {$ sisscription = new subscription(new dateTimeImmutable()); $ sisscription-> activate(); 返回$订阅; } }
笔记
这种方法提高了可读性并澄清了分离(代码的读多于写)。
尽管私人助手提供了明确的意图,但在每个测试方法中使用它们可能很乏味。
要在多个测试类之间共享相似的测试对象,请使用:
对象母亲
建设者
[!警告|样式:平面|标签:不好]
最终班级客户 {私有自定义类型$ type; private discountCalculationPolicyInterface $ discountCalculationPolicy; public function __construct() {$ this-> type = customerType :: normal(); $ this-> discountCalculationPolicy = new normalDiscountPolicy(); } public函数makevip():void {$ this-> type = customertype :: vip(); $ this-> discountCalculationPolicy = new vipdiscountpolicy(); } public函数getCustomerType():customerType {return $ this-> type; } public函数getPercentAggedIscount():int {return $ this-> discountCalculationPolicy-> getPercentAggediscount(); } }
最后一类无效的测试柜扩展了 {public函数testmakevip():void {$ sut = new Customer(); $ sut-> makevip(); self :: assertSame(custicerType :: vip(),$ sut-> getCustomerType()); } }
[!提示|样式:平面|标签:好]
最终班级客户 {私有自定义类型$ type; private discountCalculationPolicyInterface $ discountCalculationPolicy; public function __construct() {$ this-> type = customerType :: normal(); $ this-> discountCalculationPolicy = new normalDiscountPolicy(); } public函数makevip():void {$ this-> type = customertype :: vip(); $ this-> discountCalculationPolicy = new vipdiscountpolicy(); } public函数getPercentAggedIscount():int {return $ this-> discountCalculationPolicy-> getPercentAggediscount(); } }
最终班级有效测试扩展了测试柜 {/** * @test */public函数a_vip_customer_has_a_a_25_percentage_discount():void {$ sut = new customer(); $ sut-> makevip(); self :: assertSame(25,$ sut-> sut-> sut-> getPercentCentagedIscount(25,$ sat) } }
[!注意] 仅为了验证测试中的状态而添加额外的生产代码(例如 getter getCustomerType())是一种不好的做法。它应该由另一个域有效值(在本例中为 getPercentageDiscount())进行验证。当然,有时很难找到另一种方法来验证操作,我们可能被迫添加额外的生产代码来验证测试的正确性,但我们应该尽量避免这种情况。
最终班级折现器 {公共功能计算(int $ isvipfromyears):int { 断言::大thaneq($ isvipfromyears,0);返回最小值(($ isvipfromyears * 10) + 3,80); } }
[!警告|样式:平面|标签:不好]
最后一类无效的测试柜扩展了 {/** * @dataprovider discountDataProvider */公共功能testcalculate(int $ vipdaysflom,int $ endure thecred):void {$ sut = new DiscountCalculator(); self :: assertSame($ thissertsame($ nefor :: $ thistsame(预期,$ sut->计算)($ vipdays-from)($ vipdaysfrom) ); } public函数discountdataprovider():array {return [ [0,0 * 10 + 3],//泄漏域详细信息[1,1 * 10 + 3], [5,5 * 10 + 3], [8,80] ]; } }
[!提示|样式:平面|标签:好]
最终班级有效测试扩展了测试柜 {/** * @dataprovider discountDataProvider */公共功能testcalculate(int $ vipdaysflom,int $ endure thecred):void {$ sut = new DiscountCalculator(); self :: assertSame($ thissertsame($ nefor :: $ thistsame(预期,$ sut->计算)($ vipdays-from)($ vipdaysfrom) ); } public函数discountdataprovider():array {return [ [0,3], [1,13], [5,53], [8,80] ]; } }
笔记
不要在测试中重复生产逻辑。只需通过硬编码值验证结果即可。
[!警告|样式:平面|标签:不好]
班级折扣计算机 {公共功能计算内部discount(int $ isvipfromyears):int { 断言::大thaneq($ isvipfromyears,0);返回最小值(($ isvipfromyears * 10) + 3,80); } public函数calculateadDitionAldiscountSternalsystem():int {//从外部系统获取数据以计算折叠5; } }
班订单服务 {public函数__construct(private Readonly discountCalculator $ discountCalculator){} public函数getTotalPriceWithDiscount(int $ toculprice,int $ vipfromdays):int {$ interndiscount = $ nestintdiscount = $ this> $ discountcalculator- $ this-> discountCalculator-> calculateadDitionAldiscountFromeXternalsystem(); $ discountSum = $ nisternDiscount + $ nestintDiscount + $ externaldiscount; return $ totalprice-(int)ceil(($ totalprice * $ discountsum) / 100) / 100); } }
最后一类无效的测试柜扩展了 {/** * @dataProvider orderDataProvider */public function testGetTotalPriceWithDiscount(int $totalPrice, int $vipDaysFrom, int $expected): void{$discountCalculator = $this->createPartialMock(DiscountCalculator::class, ['calculateadditionaldiscountfromexternalsystem']); $ discountCalculator->方法('calculateadeadeDitionalDiscountfromexternalsystemStem') - > willeTreturn(5); $ sut = new orderService(new orderService) ,$ vipdaysfrom)); } public函数ringdataprovider():array {return [ [1000,0,920], [500,1,410], [644,5,270], ]; } }
[!提示|样式:平面|标签:好]
接口外部discountCalculatorInterface {公共功能计算():int; }
最后一类内部识别器 {公共功能计算(int $ isvipfromyears):int { 断言::大thaneq($ isvipfromyears,0);返回最小值(($ isvipfromyears * 10) + 3,80); } }
最终班订单 {public function __construct(private readonly InternalDiscountCalculator $discountCalculator,private readonly ExternalDiscountCalculatorInterface $externalDiscountCalculator) {}public function getTotalPriceWithDiscount(int $totalPrice, int $vipFromDays): int{$internalDiscount = $this->discountCalculator->calculate($vipFromDays); $ externalDiscount = $ this-> externaldiscountCalculator-> calculate(); $ discountSum = $ internationDiscount + $ nofternDiscount; return $ totalprice-(int)ceil(($ totalprice * $ discountSum) / 100); } }
最终班级有效测试扩展了测试柜 {/** * @dataprovider orderdatapRovider */公共功能testgetTotalPriceWithDiscount(int $ totureprice,int int $ vipdaysfrom,int $预期):void {$ externalDiscountCalculator = new class(new class)new class(new class()new class() } }; $ sut = new orderService(new InternalDiscountCalculator(),$ externaldiscountCalculator); self :: assertSame($ thosertsame($ thise,$ sut-> getTotAtalPriceWithDiscount($ totalprice,$ totalprice,$ totalprice,$ vipdaysfrom)); } public函数ringdataprovider():array {return [ [1000,0,920], [500,1,410], [644,5,270], ]; } }
笔记
需要模拟具体类来替换其部分行为,这意味着该类可能过于复杂并且违反了单一职责原则。
最后一堂课订购 {public函数__construct(public readonly int $ total){} }
最终班级顺序 {/** * @param orderItem [] $ items * @param int $ transportCost */public函数__construct(private Array $ tocks,private int int $ transportCost){} public fumply getTotal() () + $ this-> transportCost; }私有函数getItemStotal():int {return array_reduce(array_map(fn(orderitem $ item)=> $ item-> $ tocal-> total,$ this-> items),fn(int $ sum,int $ supation => $ sum + sum + sum + = $总计,0); } }
[!警告|样式:平面|标签:不好]
最后一类无效的测试柜扩展了 {/** * @test * @dataprovider ordersDataProvider */public函数get_total_returns_a_a_total_cost_cost_af_a_a_whole_order(订购$订单,int $ enduret $ endurettotal):void {foref) }/** * @test * @dataprovider orderitemsdataprovider */public函数get_items_total_returns_a_total_cost_of_all_items(订单$订购,int $ endure turecrettotal):void :: void {self assertSame :: $ texchertSame($ expecttottottotal,$ thise) } public函数ordersdataprovider():array {return [ [新订单([[New OrderItem(20),New OrderItem(20),New OrderItem(20)],15),75], [新订单([[New OrderItem(20),New OrderItem(30),New OrderItem(40)],0),90], [新订单([[New OrderItem(99),New OrderItem(99),New OrderItem(99)],9),306] ]; } public function OrderItemsdatapRovider():array {return [ [新订单([[New OrderItem(20),New OrderItem(20),New OrderItem(20)],15),60], [新订单([[New OrderItem(20),New OrderItem(30),New OrderItem(40)],0),90], [新订单([[New OrderItem(99),New OrderItem(99),New OrderItem(99)],9),297],297] ]; }私有函数InvokePrivateMethodGetItemStotal(订单和$订单):int {$ reflection = new ReflectionClass(get_class($ order)); $ method = $ reflection-> getMethod('getitemstotal' $ method-> invokeargs($ order,[]); } }
[!提示|样式:平面|标签:好]
最终班级有效测试扩展了测试柜 {/** * @test * @dataprovider ordersDataProvider */public函数get_total_returns_a_a_total_cost_cost_af_a_a_whole_order(订购$ order,int $ enduret $ endurettotal):void {void {self :: assertSame($ expecttotal,$ enducenttotal,$ prock-prock- prock-> gettotal> gettotal(tototal); } public函数ordersdataprovider():array {return [ [新订单([[New OrderItem(20),New OrderItem(20),New OrderItem(20)],15),75], [新订单([[New OrderItem(20),New OrderItem(30),New OrderItem(40)],0),90], [新订单([[New OrderItem(99),New OrderItem(99),New OrderItem(99)],9),306] ]; } }
[!注意] 测试应该只验证公共 API。
时间是一个不稳定的依赖项,因为它是不确定的。每次调用都会返回不同的结果。
[!警告|样式:平面|标签:不好]
最后一堂课时钟 {public static dateTime | null $ currentDateTime = null; public static函数getCurrentDateTime():dateTime {if(null ==== self :: $ currentDateTime){self :: $ currentDateTime = new dateTime(); }返回self :: $ currentDateTime; } public static函数集(dateTime $ dateTime):void {self :: $ currentDateTime = $ dateTime; } public static函数reset():void {self :: $ currentDateTime = null; } }
最终班级客户 {private datetime $ createat; public函数__construct() {$ this-> createat = clock :: getCurrentDateTime(); } public函数ISVIP():bool {return $ this-> createat-> diff(clock :: getCurrentDateTime()) - > y> = 1; } }
最后一类无效的测试柜扩展了 {/** * @test */public函数a_customer_registered_more_than_a_a_one_year_oga_ogo_is_a_vip():void { clock :: set(new dateTime('2019-01-01')); $ sut = new Customer(); 时钟:: reset(); //您必须记住要重置共享状态:: asserttrue($ sut-> iSVIP()); }/** * @test */public函数a_customer_registered_less_than_a_a_a_oear_ogo_ogo_ias_is_not_a_vip():void { clock :: set(((new dateTime()) - > sub(new DateInterval('p2m'))); $ sut = new Customer(); 时钟:: reset(); //您必须记住重置共享atesself :: assertfalse($ sut-> isvip()); } }
[!提示|样式:平面|标签:好]
接口时钟接口 {public函数getCurrentTime():dateTimeImmutable; }
最终类时钟实施时钟接口 {私有函数__construct() { }公共静态函数create():self {return new self(); } public函数getCurrentTime():dateTimeImmutable {返回new dateTimeImmutable(); } }
最终类固定电路实施Clockinterface {private函数__construct(private ReadOnly dateTimeImmutable $ fixeddate){} public static函数create(dateTimeImmutable $ fielddate):self {return new new self($ fixeddate); } public函数getCurrentTime():dateTimeImmutable {return $ this-> fixeddate; } }
最终班级客户 {public函数__construct(private ReadOnly dateTimeImmutable $ createat){} public函数iSVIP(dateTimeImmutable $ currentdate):bool {return $ this-> this-> create-> createat-> diff($ currentdate) - > y> y> y> = 1; } }
最终类有效测试扩展了测试柜 {/** * @test */public函数a_customer_registered_more_than_a_a_a_oe_ago_ago_ia_a_a_a_a_vip():void {$ sut {$ sut = new Customer = new Custome(fieldClock :: create(new Datementimmmutable) asserttrue($ sut-> iSVIP(filexclock :: create(new) dateTimeImmutable('2020-01-02')) - > getCurrentTime())); }/** * @test */public函数a_customer_registered_less_than_a_a_a_oear_ogo_ogo_is_is_is_not_a_vip():void {$ sut {$ sut = new Customer = new Customer(fieldclock :: create(new new new) dateTimeImmutable('2019-01-01')) - > getCurrenttime()); self :: assertfalse($ sut-> iSVIP(fixedclock :: create :: create(new dateTimeImmutable)(new dateTimeImmutable('2019-05-02')) ))); } }
笔记
时间和随机数不应直接在域代码中生成。为了测试行为,我们必须有确定性的结果,因此我们需要将这些值注入到域对象中,如上例所示。
100% 覆盖率不是目标,甚至是不可取的,因为如果有 100% 覆盖率,测试可能会非常脆弱,这意味着重构将非常困难。突变测试可以更好地反馈测试质量。阅读更多
测试驱动的开发:以身作则 /肯特·贝克 - 经典
单元测试原理、实践和模式 / Vladimir Khorikov - 我读过的关于测试的最好的书
卡米尔·鲁钦斯基
推特: https://twitter.com/Sarvendev
博客: https://sarvendev.com/
领英: https://www.linkedin.com/in/kamilruczynski/