Nestes tempos, os benefícios de escrever testes unitários são enormes. Acho que a maioria dos projetos iniciados recentemente contém testes de unidade. Em aplicações corporativas com muita lógica de negócios, os testes unitários são os testes mais importantes, porque são rápidos e podem nos garantir instantaneamente que nossa implementação está correta. No entanto, muitas vezes vejo problemas com bons testes em projetos, embora os benefícios desses testes só sejam enormes quando você tem bons testes unitários. Então, nesses exemplos, tentarei compartilhar algumas dicas sobre o que fazer para escrever bons testes unitários.
Versão fácil de ler: https://testing-tips.sarvendev.com/
Kamil Ruczyński
Blog: https://sarvendev.com/
LinkedIn: https://www.linkedin.com/in/kamilruczynski/
Seu apoio significa o mundo para mim! Se você gostou deste guia e encontra valor no conhecimento compartilhado, considere me apoiar no BuyMeCoffee:
ou simplesmente deixar uma estrela no repositório e me seguir no Twitter e no Github para ficar por dentro de todas as atualizações. Sua generosidade alimenta minha paixão por criar conteúdo mais esclarecedor para você.
Se você tiver alguma ideia de melhoria ou um tópico sobre o qual escrever, sinta-se à vontade para preparar uma solicitação pull ou apenas me avisar.
Assine e domine os testes de unidade com meu e-book GRATUITO!
Detalhes
Ainda tenho uma longa lista de melhorias TODO neste guia sobre testes unitários e irei apresentá-las em um futuro próximo.
Introdução
Autor
Duplas de teste
Nomeação
Padrão AAA
Mãe objeto
Construtor
Afirmar objeto
Teste parametrizado
Duas escolas de testes unitários
Clássico
Zombarista
Dependências
Simulado vs Stub
Três estilos de testes unitários
Saída
Estado
Comunicação
Arquitetura funcional e testes
Comportamento observável versus detalhes de implementação
Unidade de comportamento
Padrão humilde
Teste trivial
Teste frágil
Dispositivos de teste
Testes gerais de antipadrões
Expondo estado privado
Vazamento de detalhes do domínio
Zombando de classes concretas
Testando métodos privados
O tempo como uma dependência volátil
100% de cobertura de teste não deveria ser o objetivo
Livros recomendados
Duplas de teste são dependências falsas usadas em testes.
Um manequim é uma implementação simples que não faz nada.
classe final Mailer implementa MailerInterface {função pública enviar(Mensagem $mensagem): void{ } }
Uma farsa é uma implementação simplificada para simular o comportamento original.
classe final InMemoryCustomerRepository implementa CustomerRepositoryInterface {/** * @var Cliente[] */array privado $clientes;função pública __construct() {$this->clientes = []; }loja de função pública(Cliente $cliente): void{$this->clientes[(string) $cliente->id()->id()] = $cliente; }função pública get(CustomerId $id): Cliente{if (!isset($this->customers[(string) $id->id()])) {lançar new CustomerNotFoundException(); }return $this->clientes[(string) $id->id()]; }função pública findByEmail(Email $email): Cliente{foreach ($this->customers as $customer) {if ($customer->getEmail()->isEqual($email)) {return $customer; } }lançar nova CustomerNotFoundException(); } }
Um stub é a implementação mais simples com um comportamento codificado.
classe final UniqueEmailSpecificationStub implementa UniqueEmailSpecificationInterface {função pública éUnique(Email $email): bool{return true; } }
$specificationStub = $this->createStub(UniqueEmailSpecificationInterface::class);$specificationStub->method('isUnique')->willReturn(true);
Um espião é uma implementação para verificar um comportamento específico.
classe final Mailer implementa MailerInterface {/** * @var Mensagem[] */array privado $mensagens; função pública __construct() {$this->mensagens = []; }função pública enviar(Mensagem $mensagem): void{$this->mensagens[] = $mensagem; }função pública getCountOfSentMessages(): int{return count($this->messages); } }
Um mock é uma imitação configurada para verificar chamadas de um colaborador.
$message = new Message('[email protected]', 'Test', 'Test test test');$mailer = $this->createMock(MailerInterface::class);$mailer->expects($this-> uma vez()) ->método('enviar') ->with($this->equalTo($mensagem));
[!ATTENTION] Para verificar as interações de entrada, use um stub, mas para verificar as interações de saída, use uma simulação.
Mais: Simulado vs Stub
[!AVISO|estilo:plano|rótulo:NÃO BOM]
classe final TestExample estende TestCase {/** * @test */função pública sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->method('getAll')->willReturn([$mensagem1, $mensagem2]);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('enviar') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|estilo:plano|rótulo:MELHOR]
Melhor resistência à refatoração
Usar Refactor->Rename no método específico não quebra o teste
Melhor legibilidade
Menor custo de manutenção
Não é necessário aprender essas estruturas sofisticadas de simulação
Apenas código PHP simples e simples
classe final TestExample estende TestCase {/** * @test */função pública sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$mailer = new SpyMailer();$sut = new NotificationService($mailer, $messageRepository);$sut->send(); $mailer->assertThatMessagesHaveBeenSent([$mensagem1, $mensagem2]); } }
[!AVISO|estilo:plano|rótulo:NÃO BOM]
teste de função pública(): void{$subscription = SubscriptionMother::new();$subscription->activate();self::assertSame(Status::activated(), $subscription->status()); }
[!TIP|style:flat|label:Especifique explicitamente o que você está testando]
função pública sut(): void{// sut = Sistema em teste$sut = SubscriptionMother::new();$sut->activate();self::assertSame(Status::activated(), $sut->status ()); }
[!AVISO|estilo:plano|rótulo:NÃO BOM]
função pública it_throws_invalid_credentials_exception_when_sign_in_with_invalid_credentials(): void{ }função pública testCreatingWithATooShortPasswordIsNotPossible(): void{ }função pública testDeactivateASubscription(): void{ }
[!TIP|estilo:plano|rótulo:MELHOR]
Usar sublinhado melhora a legibilidade
O nome deve descrever o comportamento, não a implementação
Use nomes sem palavras-chave técnicas. Deve ser legível para quem não é programador.
função pública sign_in_with_invalid_credentials_is_not_possible(): void{ }função pública criando_com_a_too_short_password_is_not_possible(): void{ }função pública deactivating_an_activated_subscription_is_valid(): void{ }função pública deactivating_an_inactive_subscription_is_invalid(): void{ }
Observação
Descrever o comportamento é importante para testar os cenários de domínio. Se o seu código for apenas utilitário, é menos importante.
Por que seria útil para um não programador ler testes unitários?
Se houver um projeto com lógica de domínio complexa, essa lógica deve ser muito clara para todos, para que os testes descrevam detalhes do domínio sem palavras-chave técnicas e você possa conversar com uma empresa em uma linguagem como nesses testes. Todo código relacionado ao domínio deve estar livre de detalhes técnicos. Um não programador não lerá esses testes. Se você quiser falar sobre o domínio esses testes serão úteis para saber o que esse domínio faz. Haverá uma descrição sem detalhes técnicos, por exemplo, retorna nulo, lança uma exceção, etc. Esse tipo de informação não tem nada a ver com o domínio, portanto não devemos usar essas palavras-chave.
Também é comum Dado, Quando, Então.
Separe três seções do teste:
Organizar : Coloque o sistema em teste no estado desejado. Prepare dependências, argumentos e finalmente construa o SUT.
Act : invoca um elemento testado.
Afirmar : Verifique o resultado, o estado final ou a comunicação com os colaboradores.
[!TIP|estilo:plano|rótulo:BOM]
função pública aaa_pattern_example_test(): void{//Arrange|Given$sut = SubscriptionMother::new();//Act|When$sut->activate();//Assert|Thenself::assertSame(Status::activated( ), $sut->status()); }
O padrão ajuda a criar objetos específicos que podem ser reutilizados em alguns testes. Por causa disso, a seção de organização é concisa e o teste como um todo é mais legível.
aula final SubscriptionMother {função estática pública nova(): Assinatura{return nova Assinatura(); }função estática pública ativada(): Assinatura{$subscrição = new Assinatura();$subscrição->ativar();return $subscrição; }função estática pública desativada(): Assinatura{$subscription = self::activated();$subscription->deactivate();return $subscription; } }
classe final ExemploTest {função pública example_test_with_activated_subscription(): void{$activatedSubscription = SubscriptionMother::activated();// faça algo// verifique algo}função pública example_test_with_deactivated_subscription(): void{$deactivatedSubscription = SubscriptionMother::deactivated();// faça algo // verifica algo} }
Builder é outro padrão que nos ajuda a criar objetos em testes. Comparado ao Object Mother, o Pattern Builder é melhor para criar objetos mais complexos.
classe final OrderBuilder {private DateTimeImmutable|null $createdAt = null;/** * @var OrderItem[] */private array $items = [];função pública criadaAt(DateTimeImmutable $createdAt): self{$this->createdAt = $createdAt;return $isto; }função pública withItem(string $nome, int $preço): self{$this->items[] = new OrderItem($nome, $price);return $this; }função pública build(): Ordem{ Assert::notEmpty($this->items);return new Order($this->createdAt ?? new DateTimeImmutable(),$this->items, ); } }
classe final ExemploTest estende TestCase {/** * @test */função pública example_test_with_order_builder(): void{$order = (new OrderBuilder()) ->criadoAt(new DateTimeImmutable('2022-11-10 20:00:00')) ->withItem('Item 1', 1000) ->withItem('Item 2', 2000) ->withItem('Item 3', 3000) ->build();//faça alguma coisa//verifique alguma coisa} }
O padrão de objeto Assert ajuda a escrever seções de afirmação mais legíveis. Em vez de usar algumas afirmações, podemos apenas preparar uma abstração e usar linguagem natural para descrever o resultado esperado.
classe final ExemploTest estende TestCase {/** * @test */função pública example_test_with_asserter(): void{$currentTime = new DateTimeImmutable('2022-11-10 20:00:00');$sut = new OrderService();$order = $sut ->criar($TempoAtual); OrderAsserter::assertThat($order) ->wasCreatedAt($tempoAtual) ->hasTotal(6000); } }
use PHPUnitFrameworkAssert; classe final OrderAsserter {função pública __construct(private readonly Pedido $pedido) {}função estática pública assertThat(Pedido $pedido): self{return new OrderAsserter($pedido); }função pública wasCreatedAt(DateTimeImmutable $createdAt): self{ Assert::assertEquals($createdAt, $this->order->createdAt);return $this; }função pública hasTotal(int $total): self{ Assert::assertSame($total, $this->order->getTotal());return $this; } }
O teste parametrizado é uma boa opção para testar o SUT com muitos parâmetros sem repetir o código.
Aviso
Esse tipo de teste é menos legível. Para aumentar um pouco a legibilidade, exemplos negativos e positivos devem ser divididos em testes diferentes.
classe final ExemploTest estende TestCase {/** * @test * @dataProvider getInvalidEmails */função pública detecta_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( $resultado); }/** * @test * @dataProvider getValidEmails */função pública detecta_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( $resultado); }public function getInvalidEmails(): iterable{yield 'Um email inválido sem @' => ['test'];yield 'Um email inválido sem o domínio após @' => ['test@'];yield 'Um email inválido sem TLD' => ['test@test'];//...}public function getValidEmails(): iterable{yield 'Um e-mail válido com letras minúsculas' => ['[email protected]'];yield 'Um e-mail válido com letras minúsculas e dígitos' => ['[email protected]'];yield 'Um e-mail válido com letras maiúsculas e dígitos' => ['Test123@ test.com'];//...} }
Observação
Use yield
e adicione uma descrição de texto aos casos para melhorar a legibilidade.
A unidade é uma unidade única de comportamento, podendo ser algumas classes relacionadas.
Cada teste deve ser isolado dos outros. Portanto, deve ser possível invocá-los em paralelo ou em qualquer ordem.
classe final TestExample estende TestCase {/** * @test */função pública suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = new CanAlwaysSuspendPolicy();$sut = new Subscription();$result = $sut->suspend($canAlwaysSuspendPolicy);self::assertTrue($result);self::assertSame(Status::suspend(), $sut->status()); } }
A unidade é uma única classe.
A unidade deve ser isolada de todos os colaboradores.
classe final TestExample estende TestCase {/** * @test */função pública suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = $this->createStub(SuspendingPolicyInterface::class);$canAlwaysSuspendPolicy->method('suspend')->willReturn(true);$ sut = novo Subscription();$result = $sut->suspend($canAlwaysSuspendPolicy);self::assertTrue($result);self::assertSame(Status::suspend(), $sut->status()); } }
Observação
A abordagem clássica é melhor para evitar testes frágeis.
[PENDÊNCIA]
Exemplo:
classe final NotificationService {função pública __construct(private readonly MailerInterface $mailer,private readonly MessageRepositoryInterface $messageRepository) {}função pública send(): void{$messages = $this->messageRepository->getAll();foreach ($messages as $message) { $this->mailer->enviar($mensagem); } } }
[!AVISO|estilo:plano|rótulo:RUIM]
Afirmar interações com stubs leva a testes frágeis
classe final TestExample estende TestCase {/** * @test */função pública sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->method('getAll')->willReturn([$mensagem1, $mensagem2]);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$messageRepository->expects(self::once())->method('getAll');$mailer- >espera(self::exatamente(2))->método('enviar') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|estilo:plano|rótulo:BOM]
classe final TestExample estende TestCase {/** * @test */função pública sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);// Removidas interações de declaração com o método stub$mailer->expects(self::exactly(2))-> ('enviar') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|style:flat|label:AINDA MELHOR USAR SPY]
classe final TestExample estende TestCase {/** * @test */função pública sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$mailer = new SpyMailer();$sut = new NotificationService($mailer, $messageRepository);$sut->send(); $mailer->assertThatMessagesHaveBeenSent([$mensagem1, $mensagem2]); } }
[!TIP|style:flat|label:A melhor opção]
A melhor resistência à refatoração
A melhor precisão
O menor custo de manutenção
Se for possível, você deve preferir este tipo de teste
classe final ExemploTest estende TestCase {/** * @test * @dataProvider getInvalidEmails */função pública detecta_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( $resultado); }/** * @test * @dataProvider getValidEmails */função pública detecta_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( $resultado); }função pública getInvalidEmails(): array{return [ ['teste'], ['teste@'], ['teste@teste'], //...]; }função pública getValidEmails(): array{return [ ['[email protected]'], ['[email protected]'], ['[email protected]'], //...]; } }
[!WARNING|style:flat|label:Pior opção]
Pior resistência à refatoração
Pior precisão
Maior custo de manutenção
classe final ExemploTest estende TestCase {/** * @test */função pública adicionando_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]); } }
[!ATTENTION|style:flat|label:A pior opção]
A pior resistência à refatoração
A pior precisão
O maior custo de manutenção
classe final ExemploTest estende TestCase {/** * @test */função pública sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('enviar') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!AVISO|estilo:plano|rótulo:RUIM]
classe final NameService {função pública __construct(private readonly CacheStorageInterface $cacheStorage) {}função pública loadAll(): void{$namesCsv = array_map('str_getcsv', file(__DIR__.'/../names.csv'));$names = [ ];foreach ($nomesCsv as $nomeData) {if (!isset($nomeData[0], $nomeDados[1])) {continuar; }$nomes[] = novo Nome($nomeData[0], novo Gênero($nomeData[1])); }$this->cacheStorage->store('nomes', $nomes); } }
Como testar um código como este? Isso só é possível com um teste de integração porque utiliza diretamente um código de infraestrutura relacionado a um sistema de arquivos.
[!TIP|estilo:plano|rótulo:BOM]
Como na arquitetura funcional, precisamos separar um código com efeitos colaterais e um código que contém apenas lógica.
classe final NameParser {/** * @param array<string[]> $namesData * @return Name[] */public function parse(array $namesData): array{$names = [];foreach ($namesData as $nameData) {if (!isset($nomeData[0], $nameData[1])) {continuar; }$nomes[] = novo Nome($nomeData[0], novo Gênero($nomeData[1])); }retornar $nomes; } }
classe final CsvNamesFileLoader {carga de função pública(): array{return array_map('str_getcsv', file(__DIR__.'/../names.csv')); } }
classe final ApplicationService {função pública __construct(private readonly CsvNamesFileLoader $fileLoader,private readonly NameParser $parser,private readonly CacheStorageInterface $cacheStorage) {}função pública loadNames(): void{$namesData = $this->fileLoader->load();$names = $this->parser->parse($namesData);$this->cacheStorage->store('nomes', $nomes); } }
classe final ValidUnitExampleTest estende TestCase {/** * @test */função pública parse_all_names(): void{$namesData = [ ['João', 'M'], ['Lennon', 'U'], ['Sara', 'W'] ];$sut = new NameParser();$resultado = $sut->parse($nomesData); self::assertSame( [novo nome('John', novo gênero('M')),novo nome('Lennon', novo gênero('U')),novo nome('Sarah', novo gênero('W')) ],$resultado); } }
[!AVISO|estilo:plano|rótulo:RUIM]
classe final ApplicationService {função pública __construct(private readonly SubscriptionRepositoryInterface $subscriptionRepository) {}função pública promoteSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);if (!$subscription->getStatus() ->isEqual(Status::expired())) {return false; }$subscrição->setStatus(Status::active());$subscrição->setModifiedAt(new DateTimeImmutable());return true; } }
Assinatura da aula final {função pública __construct(status privado $status, private DateTimeImmutable $modifiedAt) {}função pública getStatus(): Status{return $this->status; }função pública setStatus(Status $status): void{$this->status = $status; }função pública getModifiedAt(): DateTimeImmutable{return $this->modifiedAt; }função pública setModifiedAt(DateTimeImmutable $modifiedAt): void{$this->modifiedAt = $modifiedAt; } }
classe final InvalidTestExample estende TestCase {/** * @test */função pública renova_an_expired_subscription_is_possible(): void{$modifiedAt = new DateTimeImmutable();$expiredSubscription = new Subscription(Status::expired(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($expiredSubscription));$resultado = $sut->renewSubscription(1);self::assertSame(Status::active(), $expiredSubscription->getStatus());self::assertGreaterThan($modifiedAt, $expiredSubscription->getModifiedAt());self:: assertTrue($resultado); }/** * @test */função pública renova_an_active_subscription_is_not_possible(): void{$modifiedAt = new DateTimeImmutable();$activeSubscription = new Subscription(Status::active(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($activeSubscription));$resultado = $sut->renewSubscription(1);self::assertSame($modifiedAt, $activeSubscription->getModifiedAt());self::assertFalse($result); } função privada createRepository(Subscription $subscription): SubscriptionRepositoryInterface{retornar nova classe ($expiredSubscription) implementa SubscriptionRepositoryInterface {função pública __construct(private readonly Subscription $subscription) {} função pública findById(int $id): Assinatura{return $this->subscrição; } }; } }
[!TIP|estilo:plano|rótulo:BOM]
classe final ApplicationService {função pública __construct(private readonly SubscriptionRepositoryInterface $subscriptionRepository) {}public function restartSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);return $subscription->renew(new DateTimeImmutable( )); } }
Assinatura da aula final {Status privado $status;private DateTimeImmutable $modifiedAt;função pública __construct(DateTimeImmutable $modifiedAt) {$this->status = Status::new();$this->modifiedAt = $modifiedAt; }função pública renovar(DateTimeImmutable $modifiedAt): bool{if (!$this->status->isEqual(Status::expired())) {return false; }$this->status = Status::active();$this->modifiedAt = $modifiedAt;return true; }função pública ativa(DateTimeImmutable $modifiedAt): void{//simplificado$this->status = Status::active();$this->modifiedAt = $modifiedAt; }função pública expira(DateTimeImmutable $modifiedAt): void{//simplificado$this->status = Status::expired();$this->modifiedAt = $modifiedAt; }função pública isActive(): bool{return $this->status->isEqual(Status::active()); } }
classe final ValidTestExample estende TestCase {/** * @test */função pública renova_an_expired_subscription_is_possible(): void{$expiredSubscription = SubscriptionMother::expired();$sut = new ApplicationService($this->createRepository($expiredSubscription));$result = $sut- >renewSubscription(1);// ignora a verificação de modificadoAt pois não faz parte do comportamento observável. Para verificar esse valor, // teríamos que adicionar um getter para modificadoAt, provavelmente apenas para fins de teste.self::assertTrue($expiredSubscription->isActive());self::assertTrue($result); }/** * @test */função pública renova_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); } função privada createRepository(Subscription $subscription): SubscriptionRepositoryInterface{retornar nova classe ($expiredSubscription) implementa SubscriptionRepositoryInterface {função pública __construct(private readonly Subscription $subscription) {} função pública findById(int $id): Assinatura{return $this->subscrição; } }; } }
Observação
O primeiro modelo de assinatura tem um design ruim. Para invocar uma operação comercial, você precisa chamar três métodos. Também usar getters para verificar a operação não é uma boa prática. Nesse caso, é ignorada a verificação de uma alteração de modifiedAt
, provavelmente a configuração modifiedAt
específica durante uma operação de renovação pode ser testada com uma operação comercial de expiração. O getter para modifiedAt
não é obrigatório. Claro, há casos em que será muito difícil encontrar a possibilidade de evitar getters fornecidos apenas para testes, mas devemos sempre tentar não introduzi-los.
[!AVISO|estilo:plano|rótulo:RUIM]
classe CannotSuspendExpiredSubscriptionPolicy implementa SuspendingPolicyInterface {função pública suspender(Assinatura $subscrição, DateTimeImmutable $at): bool{if ($subscrição->isExpired()) {return false; }retornar verdadeiro; } }
classe CannotSuspendExpiredSubscriptionPolicyTest estende TestCase {/** * @test */função pública 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, new DateTimeImmutable())); }/** * @test */função pública 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())); } }
classe CannotSuspendNewSubscriptionPolicy implementa SuspendingPolicyInterface {função pública suspender(Assinatura $subscrição, DateTimeImmutable $at): bool{if ($subscrição->isNew()) {return false; }retornar verdadeiro; } }
classe CannotSuspendNewSubscriptionPolicyTest estende TestCase {/** * @test */função pública 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 DateTimeImmutável())); }/** * @test */função pública 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 DateTimeImmutable())); } }
classe CanSuspendAfterOneMonthPolicy implementa SuspendingPolicyInterface {public function suspend(Subscription $subscription, DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
classe CanSuspendAfterOneMonthPolicyTest estende TestCase {/** * @test */função pública 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 */função pública it_returns_false_when_a_subscription_is_not_older_than_one_month(): void{$date = new DateTimeImmutable('2021-01-29');$policy = new CanSuspendAfterOneMonthPolicy();$subscription = nova assinatura(new DateTimeImmutable('2020-01-01'));self::assertTrue($policy->suspend($subscription, $date)); } }
status da classe {private const EXPIRED = 'expired';private const ACTIVE = 'active';private const NEW = 'new';private const SUSPENDED = 'suspended';função privada __construct(string somente leitura privada $status) {$this->status = $status; }função estática pública expirada(): self{return new self(self::EXPIRED); }função estática pública ativa(): self{return new self(self::ACTIVE); }função estática pública new(): self{return new self(self::NEW); }função estática pública suspensa(): self{return new self(self::SUSPENDED); }função pública isEqual(self $status): bool{return $this->status === $status->status; } }
classe StatusTest estende TestCase {função pública testEquals(): void{$status1 = Status::active();$status2 = Status::active();self::assertTrue($status1->isEqual($status2)); }função pública testNotEquals(): void{$status1 = Status::active();$status2 = Status::expired();self::assertFalse($status1->isEqual($status2)); } }
classe SubscriptionTest estende TestCase {/** * @test */função pública suspending_a_subscription_is_possible_when_a_policy_returns_true(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'suspender')->willReturn(true);$sut = nova Assinatura(nova DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertTrue($result);self::assertTrue($sut->isSuspended()); }/** * @test */função pública suspending_a_subscription_is_not_possible_when_a_policy_returns_false(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'suspender')->willReturn(false);$sut = nova Assinatura(nova DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertFalse($result);self::assertFalse($sut->isSuspended()); }/** * @test */função pública it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1M'));$sut = new Subscription($ data);self::assertTrue($sut->isOlderThan($futureDate)); }/** * @test */função pública it_returns_false_when_a_subscription_is_not_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1D'));$sut = new Assinatura($data);self::assertTrue($sut->isOlderThan($futureDate)); } }
[!ATTENTION] Não escreva código 1:1, 1 classe: 1 teste. Isso leva a testes frágeis que tornam a refatoração difícil.
[!TIP|estilo:plano|rótulo:BOM]
classe final CannotSuspendExpiredSubscriptionPolicy implementa SuspendingPolicyInterface {função pública suspender(Assinatura $subscrição, DateTimeImmutable $at): bool{if ($subscrição->isExpired()) {return false; }retornar verdadeiro; } }
a classe final CannotSuspendNewSubscriptionPolicy implementa SuspendingPolicyInterface {função pública suspender(Assinatura $subscrição, DateTimeImmutable $at): bool{if ($subscrição->isNew()) {return false; }retornar verdadeiro; } }
a classe final CanSuspendAfterOneMonthPolicy implementa SuspendingPolicyInterface {public function suspend(Subscription $subscription, DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
Status da aula final {private const EXPIRED = 'expired';private const ACTIVE = 'active';private const NEW = 'new';private const SUSPENDED = 'suspended';função privada __construct(string somente leitura privada $status) {$this->status = $status; }função estática pública expirada(): self{return new self(self::EXPIRED); }função estática pública ativa(): self{return new self(self::ACTIVE); }função estática pública new(): self{return new self(self::NEW); }função estática pública suspensa(): self{return new self(self::SUSPENDED); }função pública isEqual(self $status): bool{return $this->status === $status->status; } }
Assinatura da aula final {status privado $status;dateTimeImmutable privado $createdAt;função pública __construct(DateTimeImmutable $createdAt) {$this->status = Status::new();$this->createdAt = $createdAt; }função pública suspender(SuspendingPolicyInterface $suspendingPolicy, DateTimeImmutable $at): bool{$resultado = $suspendingPolicy->suspend($this, $at);if ($resultado) {$this->status = Status::suspended() ; }retornar $resultado; }função pública isOlderThan(DateTimeImmutable $date): bool{return $this->createdAt < $date; }função pública ativar(): void{$this->status = Status::active(); }função pública expira(): void{$this->status = Status::expired(); }função pública isExpired(): bool{return $this->status->isEqual(Status::expired()); }função pública isActive(): bool{return $this->status->isEqual(Status::active()); }função pública isNew(): bool{return $this->status->isEqual(Status::new()); }função pública isSuspended(): bool{return $this->status->isEqual(Status::suspended()); } }
classe final SubscriptionSuspendingTest estende TestCase {/** * @test */função pública suspending_an_expired_subscription_with_cannot_suspend_expired_policy_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$sut->expire();$result = $sut ->suspender(new CannotSuspendExpiredSubscriptionPolicy(), novo DateTimeImmutable());self::assertFalse($resultado); }/** * @test */função pública 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($resultado); }/** * @test */função pública suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy() , novo DateTimeImmutable());self::assertTrue($resultado); }/** * @test */função pública suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendExpiredSubscriptionPolicy() , novo DateTimeImmutable());self::assertTrue($resultado); }/** * @test */função pública suspending_an_subscription_before_a_one_month_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable('2020-01-01'));$result = $sut->suspend(new CanSuspendAfterOneMonthPolicy(), novo DateTimeImmutable('2020-01-10'));self::assertFalse($resultado); }/** * @test */função pública suspending_an_subscription_after_a_one_month_is_possible(): void{$sut = new Subscription(new DateTimeImmutable('2020-01-01'));$result = $sut->suspend(new CanSuspendAfterOneMonthPolicy(), novo DateTimeImmutable('2020-02-02'));self::assertTrue($resultado); } }
Como testar corretamente uma classe como esta?
classe ApplicationService {função pública __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'] === 'aceito') {$pedido->setStatus('pago'); }$this->formRepository->save($form);$this->orderRepository->save($order); }função privada getSoapClient(): SoapClient{return new SoapClient('https://legacy_system.pl/Soap/WebService', []); } }
[!TIP|estilo:plano|rótulo:BOM]
É necessário dividir um código complicado em classes separadas.
classe final ApplicationService {função pública __construct(private readonly OrderRepositoryInterface $orderRepository,private readonly FormRepositoryInterface $formRepository,private readonly FormApiInterface $formApi,private readonly ChangeFormStatusService $changeFormStatusService) {}função pública changeFormStatus(int $orderId): void{$order = $this->orderRepository ->getById($orderId);$formulário = $this->formRepository->getByOrderId($orderId);$status = $this->formApi->getStatusByOrderId($orderId);$this->changeFormStatusService->changeStatus($order, $form, $status);$ this->formRepository->save($form);$this->orderRepository->save($order); } }
classe final ChangeFormStatusService {função pública changeStatus(Order $order, Form $form, string $formStatus): void{$status = FormStatus::createFromString($formStatus);$form->changeStatus($status);if ($form->isAccepted( )) {$order->changeStatus(OrderStatus::paid()); } } }
classe final ChangingFormStatusTest estende TestCase {/** * @test */função pública alterando_a_form_status_to_accepted_changes_an_order_status_to_paid(): void{$order = new Order();$form = new Form();$status = 'aceito';$sut = new ChangeFormStatusService();$sut ->changeStatus($pedido, $formulário, $status);self::assertTrue($form->isAccepted());self::assertTrue($order->isPaid()); }/** * @test */função pública alterando_a_form_status_to_refused_not_changes_an_order_status(): void{$order = new Order();$form = new Form();$status = 'new';$sut = new ChangeFormStatusService();$sut ->changeStatus($pedido, $formulário, $status);self::assertFalse($form->isAccepted());self::assertFalse($order->isPaid()); } }
No entanto, ApplicationService provavelmente deve ser testado por um teste de integração apenas com FormApiInterface simulado.
[!AVISO|estilo:plano|rótulo:RUIM]
aula final Cliente {função pública __construct(string privada $nome) {}função pública getNome(): string{return $este->nome; }função pública setNome(string $nome): void{$this->nome = $nome; } }
classe final CustomerTest estende TestCase {função pública testSetName(): void{$customer = new Customer('Jack');$customer->setName('John');self::assertSame('John', $customer->getName()); } }
aula final EventSubscriber {função estática pública getSubscribedEvents(): array{return ['event' => 'onEvent']; }função pública onEvent(): void{ } }
classe final EventSubscriberTest estende TestCase {função pública testGetSubscribedEvents(): void{$result = EventSubscriber::getSubscribedEvents();self::assertSame(['event' => 'onEvent'], $resultado); } }
[!ATTENTION] Testar o código sem qualquer lógica complicada não faz sentido, mas também leva a testes frágeis.
[!AVISO|estilo:plano|rótulo:RUIM]
classe final UserRepository {função pública __construct(conexão somente leitura privada $conexão) {}função pública getUserNameByEmail(string $email): ?array{return $this->connection->createQueryBuilder() ->from('usuário', 'u') ->where('u.email = :email') ->setParameter('email', $email) ->executar() ->buscar(); } }
classe final TestUserRepository estende TestCase {função pública testGetUserNameByEmail(): void{$email = '[email protected]';$connection = $this->createMock(Connection::class);$queryBuilder = $this->createMock(QueryBuilder::class); $resultado = $this->createMock(ResultStatement::class);$userRepository = novo UserRepository($connection);$connection->espera($this->once()) ->método('createQueryBuilder') ->willReturn($queryBuilder);$queryBuilder->espera($this->once()) ->método('de') ->com('usuário', 'u') ->willReturn($queryBuilder);$queryBuilder->espera($this->once()) ->método('onde') ->with('u.email = :email') ->willReturn($queryBuilder);$queryBuilder->espera($this->once()) ->método('setParameter') ->com('e-mail', $e-mail) ->willReturn($queryBuilder);$queryBuilder->espera($this->once()) -> Método ('Execute') -> willreng ($ resultado); $ resultado-> espera ($ this-> uma vez ()) -> Método ('Fetch') -> WillReturn (['email' => $ email]); $ resultado = $ userRepository-> getUserNameByEmail ($ email); self :: assertSame (['email' => $ email], $ resultado); } }
[!ATTENTION] Testar repositórios dessa forma leva a testes frágeis e então a refatoração é difícil. Para testar repositórios, escreva testes de integração.
[!TIP|estilo:plano|rótulo:BOM]
A classe final GoodTest estende o teste {Subscrição privada Factory $ SUT; Public Function Setup (): void {$ this-> sut = new SubscriptionFactory (); }/** * @Test */Public Função Crians_a_subScription_for_a_given_date_range (): void {$ resultado = $ this-> sut-> create (new DatETimeImutable (), novo DatEtimeimutable ('agora +1 ano'); (Assinatura :: classe, $ resultado); }/** * @test */função pública throws_an_exception_on_invalid_date_range (): void {$ this-> expectException (CreateSubscriptionException :: classe); $ resultado = $ this-> sut-> create (new DateTimeImutable ('agora -1 ano'), new DateTimeImutable ()); } }
Observação
O melhor caso para usar o método setUp será testar objetos sem estado.
Qualquer configuração feita dentro setUp
une os testes e tem impacto em todos os testes.
É melhor evitar um estado compartilhado entre testes e configurar o estado inicial de acordo com o método de teste.
A legibilidade é pior em comparação com a configuração feita no método de teste adequado.
[!TIP|estilo:plano|rótulo:MELHOR]
A classe final BEGTTEST estende o TestCase {/** * @test */função pública Suspender_AN_Active_SubScription_With_Cannot_susprend_new_policy_is_possible (): void {$ sut = $ isto-> createanActiveSubScription (); $ resultado = $ Sutic. : assertTrue ($ resultado); }/** * @test */função pública Suspender_AN_Active_SubScription_With_Cannot_susprend_expired_policy_is_possible (): void {$ sut = $ new DatEnAndEnActiveSbcription (); $ resultado = $ sut-> : assertTrue ($ resultado); }/** * @test */função pública Suspender_A_New_SubScription_With_Cannot_suspend_New_Policy_is_Not_Possible (): void {$ sut = $ cangonSpendNewSubSubSccription (); $ resultado = $ SUT-> SUSTNEN (NEWNOTSSPENDNENCNENHSUNDN (); : assertfalse ($ resultado); } função privada CreateaneWSubScription (): assinatura {return nova assinatura (new DatETimeImutable ()); } função privada CreateAnActiveSubScription (): Subscription {$ Subscription = new Subscription (new DatETimeImutable ()); $ Subscription-> Activate (); Retornar $ assinatura; } }
Observação
Essa abordagem melhora a legibilidade e esclarece a separação (o código é mais lido do que escrito).
Ajudantes privados podem ser entediantes de usar em cada método de teste, embora forneçam intenções explícitas.
Para compartilhar objetos de teste semelhantes entre várias classes de teste, use:
Mãe objeto
Construtor
[!AVISO|estilo:plano|rótulo:RUIM]
Cliente de classe final {private clientetype $ type; private descontoCalculationPolicyInterface $ desconhectCalCulationPolicy; Public Função __construct () {$ this-> type = CustomerType :: Normal (); $ this-> desconhectCalCulationPolicy = new NormDiscountPolicy (); } função pública makevip (): void {$ this-> type = CustomerType :: VIP (); $ this-> descontoCalCulationPolicy = new VIPDiscountPolicy (); } função pública getCustomerType (): CustomerType {return $ this-> type; } função pública getPerCentediscount (): int {return $ this-> desconhectCalCulocationPolicy-> getPercentAgediscount (); } }
Classe final Invalidtest estende o teste {função pública testMakevip (): void {$ sut = new Customer (); $ sut-> makevip (); self :: assertsame (clientetype :: vip (), $ sut-> getCustomerType ()); } }
[!TIP|estilo:plano|rótulo:BOM]
Cliente de classe final {private clientetype $ type; private descontoCalculationPolicyInterface $ desconhectCalCulationPolicy; Public Função __construct () {$ this-> type = CustomerType :: Normal (); $ this-> desconhectCalCulationPolicy = new NormDiscountPolicy (); } função pública makevip (): void {$ this-> type = CustomerType :: VIP (); $ this-> descontoCalCulationPolicy = new VIPDiscountPolicy (); } função pública getPerCentediscount (): int {return $ this-> desconhectCalCulocationPolicy-> getPercentAgediscount (); } }
Classe final ValidTest estende o teste {/** * @test */função pública a_vip_customer_has_a_25_percentage_discount (): void {$ sut = new Customer (); $ sut-> makevip (); self :: AssertSame (25, $ sut-> getPerCerediscount ()); } }
[!ATTENTION] Adicionar código de produção adicional (por exemplo, getter getCustomerType()) apenas para verificar o estado nos testes é uma prática ruim. Deve ser verificado por outro valor significativo de domínio (neste caso getPercentageDiscount()). Claro, às vezes pode ser difícil encontrar outra maneira de verificar a operação, e podemos ser forçados a adicionar código de produção adicional para verificar a exatidão dos testes, mas devemos tentar evitar isso.
Classe Final DiscountCalculator {Função pública Calcule (int $ isvipfromyears): int { Assert :: GORADTHANEQ ($ isvipFromyears, 0); retorno min (($ isvipfromyears * 10) + 3, 80); } }
[!AVISO|estilo:plano|rótulo:RUIM]
Classe final Invalidtest estende o teste {/** * @DataProvider DOLUGODATAPROVER */função pública testCalCulate (int $ vipDaysFrom, int $ esperado): void {$ sut = novo descontoCalculator (); self :: Assertsame ($ esperado, $ SUT-> Calcule ($ VIPDAYSFROM) ); } função pública descontDataProvider (): Array {return [ [0, 0 * 10 + 3], // Detalhes do domínio vazando [1, 1 * 10 + 3], [5, 5 * 10 + 3], [8, 80] ]; } }
[!TIP|estilo:plano|rótulo:BOM]
Classe final ValidTest estende o teste {/** * @DataProvider DOLUGODATAPROVER */função pública testCalCulate (int $ vipDaysFrom, int $ esperado): void {$ sut = novo descontoCalculator (); self :: Assertsame ($ esperado, $ SUT-> Calcule ($ VIPDAYSFROM) ); } função pública descontDataProvider (): Array {return [ [0, 3], [1, 13], [5, 53], [8, 80] ]; } }
Observação
Não duplique a lógica de produção nos testes. Basta verificar os resultados por valores codificados.
[!AVISO|estilo:plano|rótulo:RUIM]
Classe descontoCalculator {Função pública CalcularInternCount (int $ isvipFromyEars): int { Assert :: GORADTHANEQ ($ isvipFromyears, 0); retorno min (($ isvipfromyears * 10) + 3, 80); } função pública CalculateAdditionAddiscountFromexTalnalSystem (): int {// Obtenha dados de um sistema externo para calcular um DiscountReturn 5; } }
classe OrderService {função pública __construct (private readonly descontcalculator $ desconhecido) {} função pública getTotalPriceWithDiscount (int $ totalPrice, int $ VIPFromDays): int {$ InternalDiscount = $ this-> desconhecido $ this-> desconhectCalculator-> calculateAdditionAddiscountFromexternalSystem (); $ desconto = $ InternalDiscount + $ externaldiscount; retorno $ totalprice-(int) teto (($ totalPrice * $ desconto) / 100); } }
Classe final Invalidtest estende o teste {/** * @DataProvider OrderDataProvider */Public Função TestGetToTalPriceWithDiscount (int $ totalPrice, int $ vipDaysFrom, int $ esperado): void {$ descontcalculator = $ this-> CreatePartialMock (descontoCalculator :: Class, Classe, Classe, Classe, Classe, ['calculateAdditionAddiscountFromexternalSystem']); $ desconhectCalculator-> Método ('CalculateAdditionAddiscountFromexternalSystem')-> WillReturn (5); $ SUT = New OrderService ($ desconhecido); Self :: Assertsame ($ esperado, $ Suts-> , $ vipDaysFrom)); } função pública OrderDataProvider (): Array {return [ [1000, 0, 920], [500, 1, 410], [644, 5, 270], ]; } }
[!TIP|estilo:plano|rótulo:BOM]
Interface ExternalDiscountCalculatorInterface {função pública Calcule (): int; }
Classe final InternalDiscountCalculator {Função pública Calcule (int $ isvipfromyears): int { Assert :: GORADTHANEQ ($ isvipFromyears, 0); retorno min (($ isvipfromyears * 10) + 3, 80); } }
Tervice final de classe {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-> calcular (); $ desconto = $ InternalDiscount + $ externaldiscount; retorno $ totalPrice-(int) teto (($ totalPrice * $ desconto) / 100); } }
Classe final ValidTest estende o teste {/** * @DataProvider OrderDataProvider */Public Função TestGetToTalPriceWithDiscount (int $ totalPrice, int $ VIPDAYSFROM, int $ esperado): void {$ externCountCalculator = nova classe () IMPRESTEMMENTO EXTERNALDCALCULatorInterface {function cálculo (função): } }; $ sut = new OrderService (novo InternalDiscountCalculator (), $ externaldiscountCalculator); self :: assertSame ($ esperado, $ sut-> getTotalPriceWithDiscount ($ totalPrice, $ VIPDAYSFROM)); } função pública OrderDataProvider (): Array {return [ [1000, 0, 920], [500, 1, 410], [644, 5, 270], ]; } }
Observação
A necessidade de zombar de uma classe concreta para substituir uma parte do seu comportamento significa que esta classe é provavelmente muito complicada e viola o Princípio da Responsabilidade Única.
Ordem de classe final {função pública __construct (public readonly int $ total) {} }
Ordem de classe final {/** * @param orderItem [] $ itens * @param int $ transportCost */função pública __construct (matriz privada $ itens, private int $ transportcost) {} função public GetTotal (): int {return $ this-> getItemstotal () + $ this-> TransportCost; } função privada getItemstotal (): int {return Array_reduce (Array_map (fn (OrderItem $ item) => $ item-> Total, $ this-> itens), fn (int $ sum, int $ total) => $ sum + = $ total, 0); } }
[!AVISO|estilo:plano|rótulo:RUIM]
Classe final Invalidtest estende o teste {/** * @test * @dataprovider ordersdataprovider */função pública get_total_returns_a_total_cost_of_a_whole_order (order $ order, int $ esperaTotal): void {self :: }/** * @Test * @DataProvider OrderItemsDataProvider */Public Função get_items_total_returns_a_total_cost_of_all_items (order $ order, int $ esperatotal): void {Self :: AsserSame ($ esperado } função pública OrdensDataProvider (): Array {return [ [New Order ([New OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 75], [New Order ([New OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [New Order ([New OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 306] ]; } função pública OrderItemsDataProvider (): Array {return [ [New Order ([New OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 60], [New Order ([New OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [New Order ([New OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 297] ]; } função privada InvokePrivatemethodgetItemstotal (Ordem & $ Order): int {$ reflexão = new ReflectionClass (get_class ($ order)); $ method = $ reflexão-> getMethod ('getItemstotal'); $ métodos-> setaccessible (true); retorno; $ métod-> InvoKeargs ($ order, []); } }
[!TIP|estilo:plano|rótulo:BOM]
Classe final ValidTest estende o teste {/** * @test * @dataprovider ordersdataprovider */função pública get_total_returns_a_total_cost_of_a_whole_order (order $ order, int $ esperaTotal): void {self :: } função pública OrdensDataProvider (): Array {return [ [New Order ([New OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 75], [New Order ([New OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [New Order ([New OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 306] ]; } }
[!ATTENTION] Os testes só devem verificar a API pública.
O tempo é uma dependência volátil porque não é determinístico. Cada invocação retorna um resultado diferente.
[!AVISO|estilo:plano|rótulo:RUIM]
Relógio de classe final {public static dateTime | null $ currentDateTime = null; função estática pública getCurrentDateTime (): DateTime {if (null === self :: $ currentDateTime) {self :: $ currentDateTime = new DateTime (); } return self :: $ currentDateTime; } Função estática pública Conjunto (DATETime $ DATETIME): void {self :: $ currentDateTime = $ DateTime; } função estática pública RESET (): void {self :: $ currentDateTime = null; } }
Cliente de classe final {Private DateTime $ CreatedAt; Public Function __construct () {$ this-> CreatedAt = relógio :: getCurrentDateTime (); } função pública isvip (): bool {return $ this-> criateat-> diff (clock :: getCurrentDateTime ())-> y> = 1; } }
Classe final Invalidtest estende o teste {/** * @test */função pública a_customer_registered_more_than_a_one_year_ago_is_a_vip (): void { Relógio :: set (new DateTime ('2019-01-01')); $ sut = novo cliente (); Relógio :: reset (); // você deve se lembrar de redefinir o States Selffelf :: AssertTrue compartilhado ($ sut-> isvip ()); am Relógio :: set ((new DateTime ())-> sub (new DateInterval ('P2M'))); $ sut = new Customer (); Relógio :: reset (); // Você deve se lembrar de redefinir o Statesfl :: Assertfalse compartilhado ($ sut-> isvip ()); } }
[!TIP|estilo:plano|rótulo:BOM]
interface ClockInterface {função pública getCurrentTime (): DATETimeImutable; }
O relógio final da classe implementa o ClockInterface {função privada __construct () { } função estática pública Crie (): self {return new self (); } função pública getCurrentTime (): DATETimeImutable {return new DatETimeImutable (); } }
Classe Final Fixerclock implementa o relógio {função privada __construct (private readOnly datEtimeImutable $ fixoDate) {} função estática pública Criar (DATETimeImutable $ fixoDate): self {return New Self ($ fixedDate); } função pública getCurrentTime (): DATETimeImutable {return $ this-> fixedDate; } }
Cliente de classe final {função pública __construct (private readOnly datEtimeImutable $ crioutAt) {} função pública ISVIP (DATETimeImutable $ currentDate): bool {return $ this-> criateat-> diff ($ currentDate)-> y> = 1; } }
Classe final ValidTest estende o teste {/** * @Test */Public Função a_customer_registered_more_than_a_one_year_ago_is_a_vip (): void {$ sut = novo cliente (fixaClock :: Create (new DatetimeImutable ('2019-01-01'))-> getCurntime (); AssertTrue ($ sut-> ISVIP (FILLCLOCK :: CREATE (NOVO DateTimeImutable ('2020-01-02'))-> getCurrentTime ())); }/** * @Test */Public Função a_customer_registered_less_than_a_one_year_ago_is_not_a_vip (): void {$ sut = novo cliente (fixaclock :: create (novo DateTimeImutable ('2019-01-01'))-> getCurrentTime ()); self :: Assertfalse ($ sut-> iSVip (FIRLCLOCK :: CREATE (NOVO DATETIMEIMUTABLE ('2019-05-02')-> getCurrentTime ( ))); } }
Observação
A hora e os números aleatórios não devem ser gerados diretamente no código do domínio. Para testar o comportamento devemos ter resultados determinísticos, então precisamos injetar esses valores em um objeto de domínio como no exemplo acima.
100% de cobertura não é o objetivo ou até mesmo é indesejável porque se houver 100% de cobertura, os testes provavelmente serão muito frágeis, o que significa que a refatoração será muito difícil. O teste de mutação fornece melhor feedback sobre a qualidade dos testes. Leia mais
Desenvolvimento orientado a testes: pelo exemplo / Kent Beck - o clássico
Princípios, práticas e padrões de testes unitários / Vladimir Khorikov - o melhor livro sobre testes que já li
Kamil Ruczyński
Twitter: https://twitter.com/Sarvendev
Blog: https://sarvendev.com/
LinkedIn: https://www.linkedin.com/in/kamilruczynski/