En estos tiempos, los beneficios de escribir pruebas unitarias son enormes. Creo que la mayoría de los proyectos iniciados recientemente contienen pruebas unitarias. En aplicaciones empresariales con mucha lógica de negocio, las pruebas unitarias son las pruebas más importantes, porque son rápidas y pueden asegurarnos instantáneamente que nuestra implementación es correcta. Sin embargo, a menudo veo un problema con las buenas pruebas en los proyectos, aunque los beneficios de estas pruebas sólo son enormes cuando tienes buenas pruebas unitarias. Entonces, en estos ejemplos, intentaré compartir algunos consejos sobre qué hacer para escribir buenas pruebas unitarias.
Versión fácil de leer: https://testing-tips.sarvendev.com/
Kamil Ruczyński
Blog: https://sarvendev.com/
LinkedIn: https://www.linkedin.com/in/kamilruczynski/
¡Su apoyo significa mucho para mí! Si ha disfrutado de esta guía y encuentra valor en el conocimiento compartido, considere apoyarme en BuyMeCoffee:
o simplemente dejar una estrella en el repositorio y seguirme en Twitter y Github para estar al día de todas las actualizaciones. Tu generosidad alimenta mi pasión por crear contenido más revelador para ti.
Si tiene alguna idea de mejora o un tema sobre el que escribir, no dude en preparar una solicitud de extracción o simplemente hágamelo saber.
¡Suscríbete y domina las pruebas unitarias con mi libro electrónico GRATUITO!
Detalles
Todavía tengo una lista bastante larga de mejoras a esta guía sobre pruebas unitarias y las presentaré en un futuro próximo.
Introducción
Autor
Prueba dobles
Nombrar
patrón AAA
madre objeto
Constructor
Afirmar objeto
Prueba parametrizada
Dos escuelas de pruebas unitarias
Clásico
burlón
Dependencias
Simulacro vs trozo
Tres estilos de pruebas unitarias
Producción
Estado
Comunicación
Arquitectura funcional y pruebas.
Comportamiento observable frente a detalles de implementación
Unidad de comportamiento
patrón humilde
prueba trivial
prueba frágil
Accesorios de prueba
Antipatrones de prueba generales
Exponiendo al estado privado
Detalles del dominio filtrados
Burlándose de clases concretas
Probar métodos privados
El tiempo como dependencia volátil
El objetivo no debería ser una cobertura de prueba del 100%
Libros recomendados
Los dobles de prueba son dependencias falsas que se utilizan en las pruebas.
Un muñeco es una implementación simple que no hace nada.
la clase final Mailer implementa MailerInterface {función pública enviar(Mensaje $mensaje): nulo{ } }
Una falsificación es una implementación simplificada para simular el comportamiento original.
La clase final InMemoryCustomerRepository implementa CustomerRepositoryInterface {/** * @var Cliente[] */matriz privada $clientes;función pública __construct() {$esto->clientes = []; }tienda de funciones públicas(Cliente $cliente): void{$this->clientes[(cadena) $cliente->id()->id()] = $cliente; }función pública get(CustomerId $id): Cliente{if (!isset($this->customers[(string) $id->id()])) {throw new CustomerNotFoundException(); }return $this->clientes[(string) $id->id()]; }función pública findByEmail(Correo electrónico $correo electrónico): Cliente{foreach ($this->clientes como $cliente) {if ($cliente->getEmail()->isEqual($correo electrónico)) {return $cliente; } }lanzar nueva CustomerNotFoundException(); } }
Un código auxiliar es la implementación más simple con un comportamiento codificado.
la clase final UniqueEmailSpecificationStub implementa UniqueEmailSpecificationInterface {función pública esUnique(Correo electrónico $correo electrónico): bool{return true; } }
$specificationStub = $this->createStub(UniqueEmailSpecificationInterface::class);$specificationStub->method('isUnique')->willReturn(true);
Un espía es una implementación para verificar un comportamiento específico.
la clase final Mailer implementa MailerInterface {/** * @var Mensaje[] */matriz privada $mensajes; función pública __construct() {$esto->mensajes = []; }función pública enviar(Mensaje $mensaje): void{$this->messages[] = $mensaje; }función pública getCountOfSentMessages(): int{return count($this->messages); } }
Un simulacro es una imitación configurada para verificar llamadas a un colaborador.
$mensaje = nuevo mensaje('[email protected]', 'Prueba', 'Prueba prueba prueba');$mailer = $this->createMock(MailerInterface::class);$mailer->expects($this-> una vez()) ->método('enviar') ->with($this->equalTo($mensaje));
[!ATTENTION] Para verificar las interacciones entrantes, use un código auxiliar, pero para verificar las interacciones salientes, use un simulacro.
Más: Mock vs Stub
[!ADVERTENCIA|estilo:plano|etiqueta:NO BUENA]
La clase final TestExample extiende TestCase {/** * @test */función pública sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->método('getAll')->willReturn([$mensaje1, $mensaje2]);$correo = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('enviar') ->withConsecutive([self::equalTo($mensaje1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|estilo:plano|etiqueta:MEJOR]
Mejor resistencia a la refactorización.
Usar Refactor->Rename en el método particular no interrumpe la prueba
Mejor legibilidad
Menor costo de mantenimiento
No es necesario aprender esos sofisticados marcos simulados.
Simplemente código PHP simple
La clase final TestExample extiende TestCase {/** * @test */función pública sends_all_notifications(): void{$message1 = nuevo mensaje();$message2 = nuevo mensaje();$messageRepository = nuevo InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$mailer = nuevo SpyMailer();$sut = nuevo NotificationService($mailer, $messageRepository);$sut->send(); $mensaje de correo->assertThatMessagesHaveBeenSent([$mensaje1, $mensaje2]); } }
[!ADVERTENCIA|estilo:plano|etiqueta:NO BUENA]
prueba de función pública(): void{$suscripción = SuscripciónMadre::nueva();$suscripción->activar();self::assertSame(Estado::activado(), $suscripción->estado()); }
[!TIP|style:flat|label:Especifique explícitamente lo que está probando]
función pública sut(): void{// sut = Sistema bajo prueba$sut = SubscriptionMother::new();$sut->activate();self::assertSame(Status::activated(), $sut->status ()); }
[!ADVERTENCIA|estilo:plano|etiqueta:NO BUENA]
función pública it_throws_invalid_credentials_exception_when_sign_in_with_invalid_credentials(): void{ }prueba de función públicaCreatingWithATooShortPasswordIsNotPossible(): void{ }función pública testDeactivateASubscription(): nulo{ }
[!TIP|estilo:plano|etiqueta:MEJOR]
El uso de guiones bajos mejora la legibilidad
El nombre debe describir el comportamiento, no la implementación.
Utilice nombres sin palabras clave técnicas. Debería ser legible para una persona que no sea programador.
función pública sign_in_with_invalid_credentials_is_not_possible(): nulo{ }función pública creando_with_a_too_short_password_is_not_possible(): void{ }función pública desactivando_an_activated_subscription_is_valid(): nula{ }función pública desactivando_an_inactive_subscription_is_invalid(): nulo{ }
Nota
Describir el comportamiento es importante para probar los escenarios del dominio. Si su código es solo de utilidad, es menos importante.
¿Por qué sería útil para alguien que no sea programador leer pruebas unitarias?
Si hay un proyecto con una lógica de dominio compleja, esta lógica debe ser muy clara para todos, de modo que las pruebas describan los detalles del dominio sin palabras clave técnicas y puedas hablar con una empresa en un lenguaje como en estas pruebas. Todo el código relacionado con el dominio debe estar libre de detalles técnicos. Alguien que no sea programador no leerá estas pruebas. Si quieres hablar del dominio estos tests te serán útiles para saber qué hace este dominio. Habrá una descripción sin detalles técnicos, por ejemplo, devuelve nulo, arroja una excepción, etc. Este tipo de información no tiene nada que ver con el dominio, por lo que no debemos usar estas palabras clave.
También es común Dado, Cuándo, Entonces.
Separe tres secciones de la prueba:
Organizar : coloque el sistema bajo prueba en el estado deseado. Preparar dependencias, argumentos y finalmente construir el SU.
Actuar : invocar un elemento probado.
Assert : Verificar el resultado, el estado final o la comunicación con los colaboradores.
[!TIP|estilo:plano|etiqueta:BUENO]
función pública aaa_pattern_example_test(): void{//Arrange|Given$sut = SubscriptionMother::new();//Act|When$sut->activate();//Assert|Thenself::assertSame(Status::activated( ), $sut->estado()); }
El patrón ayuda a crear objetos específicos que pueden reutilizarse en algunas pruebas. Por eso, la sección de organización es concisa y la prueba en su conjunto es más legible.
Suscripción de clase finalMadre {función estática pública nueva(): Suscripción{return nueva Suscripción(); }función estática pública activada(): Suscripción{$suscripción = nueva Suscripción();$suscripción->activate();return $suscripción; }función estática pública desactivada(): Suscripción{$suscripción = self::activated();$suscripción->deactivate();return $suscripción; } }
prueba de ejemplo de clase final {función pública example_test_with_activated_subscription(): void{$activatedSubscription = SubscriptionMother::activated();// hacer algo// comprobar algo}función pública example_test_with_deactivated_subscription(): void{$deactivatedSubscription = SubscriptionMother::deactivated();// hacer algo // comprobar algo} }
Builder es otro patrón que nos ayuda a crear objetos en las pruebas. En comparación con el generador de patrones Object Mother, es mejor para crear objetos más complejos.
OrderBuilder de clase final {privado DateTimeImmutable|null $createdAt = null;/** * @var OrderItem[] */matriz privada $items = [];función pública creadaAt(DateTimeImmutable $createdAt): self{$this->createdAt = $createdAt;return $esto; }función pública conItem(cadena $nombre, int $precio): self{$this->items[] = new OrderItem($nombre, $precio);return $this; }construcción de función pública(): Orden{ Assert::notEmpty($this->items);return new Order($this->createdAt ?? new DateTimeImmutable(),$this->items, ); } }
La clase final EjemploTest extiende TestCase {/** * @test */función pública example_test_with_order_builder(): void{$order = (new OrderBuilder()) ->creadoEn(nueva FechaHoraImmutable('2022-11-10 20:00:00')) ->withItem('Artículo 1', 1000) ->withItem('Artículo 2', 2000) ->withItem('Artículo 3', 3000) ->build();// hacer algo// comprobar algo} }
El patrón de objeto de afirmación ayuda a escribir secciones de afirmación más legibles. En lugar de utilizar algunas afirmaciones, podemos simplemente preparar una abstracción y utilizar lenguaje natural para describir el resultado esperado.
La clase final EjemploTest extiende TestCase {/** * @test */función pública example_test_with_asserter(): void{$currentTime = new DateTimeImmutable('2022-11-10 20:00:00');$sut = new OrderService();$order = $sut ->crear($horaactual); OrderAsserter::assertThat($pedido) ->fueCreadoEn($HoraActual) ->tieneTotal(6000); } }
utilizar PHPUnitFrameworkAssert; clase final OrderAsserter {función pública __construct(orden privada de solo lectura $orden) {}función estática pública afirmarque(orden $orden): self{return new OrderAsserter($orden); }función pública fueCreatedAt(DateTimeImmutable $createdAt): self{ Afirmar::assertEquals($createdAt, $this->order->createdAt);return $this; }función pública tieneTotal(int $total): self{ Afirmar::assertSame($total, $this->order->getTotal());return $this; } }
La prueba parametrizada es una buena opción para probar el SUT con muchos parámetros sin repetir el código.
Advertencia
Este tipo de prueba es menos legible. Para aumentar un poco la legibilidad, los ejemplos negativos y positivos deben dividirse en diferentes pruebas.
La clase final EjemploTest extiende TestCase {/** * @test * @dataProvider getInvalidEmails */función pública detecta_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$resultado = $sut->isValid($email);self::assertFalse( $resultado); }/** * @test * @dataProvider getValidEmails */función pública detecta_an_valid_email_address(cadena $correo electrónico): void{$sut = new EmailValidator();$resultado = $sut->isValid($correo electrónico);self::assertTrue( $resultado); }función pública getInvalidEmails(): iterable{yield 'Un correo electrónico no válido sin @' => ['test'];yield 'Un correo electrónico no válido sin el dominio después de @' => ['test@'];yield 'Un correo electrónico no válido without TLD' => ['test@test'];//...}función pública getValidEmails(): iterable{yield 'Un correo electrónico válido con letras minúsculas' => ['[email protected]'];yield 'Un correo electrónico válido con letras minúsculas y dígitos' => ['[email protected]'];yield 'Un correo electrónico válido con letras mayúsculas y dígitos' => ['Test123@ prueba.com'];//...} }
Nota
Utilice yield
y agregue una descripción de texto a los casos para mejorar la legibilidad.
Una unidad es una única unidad de comportamiento, pueden ser varias clases relacionadas.
Cada prueba debe aislarse de las demás. Por tanto, debe ser posible invocarlos en paralelo o en cualquier orden.
La clase final TestExample extiende TestCase {/** * @test */función pública suspensing_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = new CanAlwaysSuspendPolicy();$sut = new Subscription();$resultado $sut->suspend($canAlwaysSuspendPolicy);self::assertTrue($resultado);self::assertSame(Status::suspend(), $sut->status()); } }
La unidad es una sola clase.
La unidad deberá estar aislada de todos los colaboradores.
La clase final TestExample extiende TestCase {/** * @test */función pública suspender_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = $this->createStub(SuspendingPolicyInterface::class);$canAlwaysSuspendPolicy->method('suspend')->willReturn(true);$ sut = nuevo Suscripción();$resultado = $sut->suspend($canAlwaysSuspendPolicy);self::assertTrue($resultado);self::assertSame(Status::suspend(), $sut->status()); } }
Nota
El enfoque clásico es mejor para evitar pruebas frágiles.
[HACER]
Ejemplo:
Servicio de notificación de clase final {función pública __construct(MailerInterface privado de solo lectura $mailer, MessageRepositoryInterface privado de solo lectura $messageRepository) {}función pública enviar(): void{$mensajes = $this->messageRepository->getAll();foreach ($mensajes como $mensaje) { $this->envío de correo->enviar($mensaje); } } }
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
Afirmar interacciones con stubs conduce a pruebas frágiles
La clase final TestExample extiende TestCase {/** * @test */función pública sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->método('getAll')->willReturn([$mensaje1, $mensaje2]);$correo = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$messageRepository->expects(self::once())->method('getAll');$mailer- >espera(self::exactamente(2))->método('enviar') ->withConsecutive([self::equalTo($mensaje1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|estilo:plano|etiqueta:BUENO]
La clase final TestExample extiende TestCase {/** * @test */función pública sends_all_notifications(): void{$message1 = nuevo mensaje();$message2 = nuevo mensaje();$messageRepository = nuevo InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$correo = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);// Se eliminaron las interacciones de afirmación con el stub$mailer->expects(self::exactly(2))->método ('enviar') ->withConsecutive([self::equalTo($mensaje1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|style:flat|label:AÚN MEJOR USAR SPY]
La clase final TestExample extiende TestCase {/** * @test */función pública sends_all_notifications(): void{$message1 = nuevo mensaje();$message2 = nuevo mensaje();$messageRepository = nuevo InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$mailer = nuevo SpyMailer();$sut = nuevo NotificationService($mailer, $messageRepository);$sut->send(); $mensaje de correo->assertThatMessagesHaveBeenSent([$mensaje1, $mensaje2]); } }
[!TIP|estilo:plano|label:La mejor opción]
La mejor resistencia a la refactorización.
La mejor precisión
El menor coste de mantenibilidad.
Si es posible, deberías preferir este tipo de prueba.
La clase final EjemploTest extiende TestCase {/** * @test * @dataProvider getInvalidEmails */función pública detecta_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$resultado = $sut->isValid($email);self::assertFalse( $resultado); }/** * @test * @dataProvider getValidEmails */función pública detecta_an_valid_email_address(cadena $correo electrónico): void{$sut = new EmailValidator();$resultado = $sut->isValid($correo electrónico);self::assertTrue( $resultado); }función pública getInvalidEmails(): matriz{return [ ['prueba'], ['prueba@'], ['prueba@prueba'],//...]; }función pública getValidEmails(): matriz{return [ ['[email protected]'], ['[email protected]'], ['[email protected]'],//...]; } }
[!WARNING|estilo:plano|etiqueta:Peor opción]
Peor resistencia a la refactorización.
Peor precisión
Mayor costo de mantenimiento
La clase final EjemploTest extiende TestCase {/** * @test */función pública add_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]); } }
[!ATENCIÓN|estilo:plano|etiqueta:La peor opción]
La peor resistencia a la refactorización.
La peor precisión
El mayor coste de mantenibilidad.
La clase final EjemploTest extiende TestCase {/** * @test */función pública sends_all_notifications(): void{$message1 = nuevo mensaje();$message2 = nuevo mensaje();$messageRepository = nuevo InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$correo = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('enviar') ->withConsecutive([self::equalTo($mensaje1)], [self::equalTo($message2)]);$sut->send(); } }
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
Servicio de nombres de clase final {función pública __construct(CacheStorageInterface privada de solo lectura $cacheStorage) {}función pública loadAll(): void{$namesCsv = array_map('str_getcsv', file(__DIR__.'/../names.csv'));$names = [ ];foreach ($nombresCsv como $nombreDatos) {if (!isset($nombreDatos[0], $nombreDatos[1])) {continuar; }$nombres[] = nuevo Nombre($nombreDatos[0], nuevo Género($nombreDatos[1])); }$this->cacheStorage->store('nombres', $nombres); } }
¿Cómo probar un código como este? Sólo es posible con una prueba de integración porque utiliza directamente un código de infraestructura relacionado con un sistema de archivos.
[!TIP|estilo:plano|etiqueta:BUENO]
Al igual que en la arquitectura funcional, necesitamos separar un código con efectos secundarios y un código que contiene solo lógica.
clase final NameParser {/** * @param array<string[]> $namesData * @return Name[] */public function parse(array $namesData): array{$names = [];foreach ($namesData as $nameData) {if (!isset($nombreDatos[0], $nombreDatos[1])) {continuar; }$nombres[] = nuevo Nombre($nombreDatos[0], nuevo Género($nombreDatos[1])); }retornar $nombres; } }
clase final CsvNamesFileLoader {carga de función pública(): matriz{return array_map('str_getcsv', file(__DIR__.'/../names.csv')); } }
Servicio de aplicación de clase final {función pública __construct(CsvNamesFileLoader privado de solo lectura $fileLoader,NameParser privado de solo lectura $parser,CacheStorageInterface privado de solo lectura $cacheStorage) {}función pública loadNames(): void{$namesData = $this->fileLoader->load();$names = $this->parser->parse($namesData);$this->cacheStorage->store('nombres', $nombres); } }
la clase final ValidUnitExampleTest extiende TestCase {/** * @test */función pública parse_all_names(): void{$namesData = [ ['Juan', 'M'], ['Lennon', 'U'], ['Sara', 'W'] ];$sut = new NameParser();$resultado = $sut->parse($namesData); self::afirmarIgual( [nuevo nombre ('John', nuevo género ('M')), nuevo nombre ('Lennon', nuevo género ('U')), nuevo nombre ('Sarah', nuevo género ('W')) ],$resultado); } }
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
Servicio de aplicación de clase final {función pública __construct(privado solo lectura SubscriptionRepositoryInterface $subscriptionRepository) {}función pública renewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);if (!$subscription->getStatus() ->isEqual(Estado::expirado())) {devuelve falso; }$suscripción->setStatus(Estado::activo());$suscripción->setModifiedAt(new DateTimeImmutable());devuelve verdadero; } }
Suscripción clase final {función pública __construct(Estado privado $estado, DateTimeImmutable privado $modifiedAt) {}función pública getStatus(): Estado{return $this->status; }función pública setStatus(Estado $estado): void{$this->status = $estado; }función pública getModifiedAt(): DateTimeImmutable{return $this->modifiedAt; }función pública setModifiedAt(DateTimeImmutable $modifiedAt): void{$this->modifiedAt = $modifiedAt; } }
la clase final InvalidTestExample extiende TestCase {/** * @test */función pública renew_an_expired_subscription_is_possible(): void{$modifiedAt = new DateTimeImmutable();$expiredSubscription = new Subscription(Status::expired(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($suscripciónexpirada));$resultado = $sut->renewSubscription(1);self::assertSame(Status::active(), $expiredSubscription->getStatus());self::assertGreaterThan($modifiedAt, $expiredSubscription->getModifiedAt());self:: afirmarVerdadero($resultado); }/** * @test */función pública renew_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($resultado); } función privada createRepository(Suscripción $suscripción): SubscriptionRepositoryInterface{devuelve nueva clase ($expiredSubscription) implementa SubscriptionRepositoryInterface {función pública __construct(suscripción privada de solo lectura $suscripción) {} función pública findById(int $id): Suscripción{return $this->suscripción; } }; } }
[!TIP|estilo:plano|etiqueta:BUENO]
Servicio de aplicación de clase final {función pública __construct(privado solo lectura SubscriptionRepositoryInterface $subscriptionRepository) {}función pública renewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);return $subscription->renew(new DateTimeImmutable( )); } }
Suscripción clase final {Estado privado $estado;DateTimeImmutable $modifiedAt privado;función pública __construct(DateTimeImmutable $modifiedAt) {$this->status = Estado::new();$this->modifiedAt = $modifiedAt; }renovación de función pública(DateTimeImmutable $modifiedAt): bool{if (!$this->status->isEqual(Status::expired())) {return false; }$this->status = Estado::activo();$this->modifiedAt = $modifiedAt;return true; }función pública activa(DateTimeImmutable $modifiedAt): void{//simplified$this->status = Status::active();$this->modifiedAt = $modifiedAt; }función pública expirar(DateTimeImmutable $modifiedAt): void{//simplified$this->status = Status::expired();$this->modifiedAt = $modifiedAt; }función pública isActive(): bool{return $this->status->isEqual(Status::active()); } }
la clase final ValidTestExample extiende TestCase {/** * @test */función pública renew_an_expired_subscription_is_possible(): void{$expiredSubscription = SubscriptionMother::expired();$sut = new ApplicationService($this->createRepository($expiredSubscription));$resultado = $sut- >renewSubscription(1);// omita la verificación de modificó ya que no es parte del comportamiento observable. Para verificar este valor, // tendríamos que agregar un captador para modificóAt, probablemente solo para fines de prueba.self::assertTrue($expiredSubscription->isActive());self::assertTrue($result); }/** * @test */función pública renew_an_active_subscription_is_not_possible(): void{$activeSubscription = SubscriptionMother::active();$sut = new ApplicationService($this->createRepository($activeSubscription));$resultado = $sut->renewSubscription(1);self::assertTrue($activeSubscription->isActive());self::assertFalse($resultado); } función privada createRepository(Suscripción $suscripción): SubscriptionRepositoryInterface{devuelve nueva clase ($expiredSubscription) implementa SubscriptionRepositoryInterface {función pública __construct(suscripción privada de solo lectura $suscripción) {} función pública findById(int $id): Suscripción{return $this->suscripción; } }; } }
Nota
El primer modelo de suscripción tiene un mal diseño. Para invocar una operación comercial es necesario llamar a tres métodos. Además, utilizar captadores para verificar la operación no es una buena práctica. En este caso, se omitió la verificación de un cambio de modifiedAt
, probablemente la configuración de modifiedAt
específica durante una operación de renovación se puede probar con una operación comercial de vencimiento. El captador de modifiedAt
no es necesario. Por supuesto, hay casos en los que será muy difícil encontrar la posibilidad de evitar los captadores proporcionados sólo para las pruebas, pero siempre debemos intentar no introducirlos.
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
la clase CannotSuspendExpiredSubscriptionPolicy implementa SuspendingPolicyInterface {función pública suspender(Suscripción $suscripción, DateTimeImmutable $at): bool{if ($suscripción->isExpired()) {return false; }devuelve verdadero; } }
la clase CannotSuspendExpiredSubscriptionPolicyTest extiende TestCase {/** * @test */función 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 */función 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())); } }
la clase CannotSuspendNewSubscriptionPolicy implementa SuspendingPolicyInterface {función pública suspender(Suscripción $suscripción, DateTimeImmutable $at): bool{if ($suscripción->isNew()) {return false; }devuelve verdadero; } }
la clase CannotSuspendNewSubscriptionPolicyTest extiende TestCase {/** * @test */función 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($política->suspender($suscripción, nuevo DateTimeImmutable())); }/** * @test */función 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())); } }
la clase CanSuspendAfterOneMonthPolicy implementa SuspendingPolicyInterface {función pública suspender(Suscripción $suscripción, DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
la clase CanSuspendAfterOneMonthPolicyTest extiende TestCase {/** * @test */función 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($suscripción, $fecha)); }/** * @test */función 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 = new Suscripción(new DateTimeImmutable('2020-01-01'));self::assertTrue($policy->suspend($suscripción, $fecha)); } }
estado de clase {const privado EXPIRED = 'caducado'; const privado ACTIVO = 'activo'; const privado NUEVO = 'nuevo'; const privado SUSPENDIDO = 'suspendido'; función privada __construct(cadena privada de solo lectura $estado) {$este->estado = $estado; }función estática pública caducada(): self{return new self(self::EXPIRED); }función estática pública activa(): self{return new self(self::ACTIVE); }función estática pública nueva(): self{return new self(self::NEW); }función estática pública suspendida(): self{return new self(self::SUSPENDED); }función pública esEqual(self $estado): bool{return $this->status === $status->status; } }
la clase StatusTest extiende TestCase {función pública testEquals(): void{$status1 = Estado::activo();$status2 = Estado::activo();self::assertTrue($status1->isEqual($status2)); }función pública testNotEquals(): void{$status1 = Estado::activo();$status2 = Estado::expirado();self::assertFalse($status1->isEqual($status2)); } }
clase SubscriptionTest extiende TestCase {/** * @test */función pública suspender_a_subscription_is_possible_when_a_policy_returns_true(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'suspender')->willReturn(true);$sut = nueva Suscripción(nueva DateTimeImmutable());$resultado = $sut->suspend($policy, new DateTimeImmutable());self::assertTrue($resultado);self::assertTrue($sut->isSuspended()); }/** * @test */función pública suspender_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 = nueva Suscripción(nueva DateTimeImmutable());$resultado = $sut->suspend($policy, new DateTimeImmutable());self::assertFalse($resultado);self::assertFalse($sut->isSuspended()); }/** * @test */función 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($ fecha);self::assertTrue($sut->isOlderThan($futureDate)); }/** * @test */función 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 Suscripción($fecha);self::assertTrue($sut->isOlderThan($futureDate)); } }
[!ATENCIÓN] No escriba código 1:1, 1 clase: 1 prueba. Conduce a pruebas frágiles que hacen que la refactorización sea difícil.
[!TIP|estilo:plano|etiqueta:BUENO]
La clase final CannotSuspendExpiredSubscriptionPolicy implementa SuspendingPolicyInterface {función pública suspender(Suscripción $suscripción, DateTimeImmutable $at): bool{if ($suscripción->isExpired()) {return false; }devuelve verdadero; } }
La clase final CannotSuspendNewSubscriptionPolicy implementa SuspendingPolicyInterface {función pública suspender(Suscripción $suscripción, DateTimeImmutable $at): bool{if ($suscripción->isNew()) {return false; }devuelve verdadero; } }
La clase final CanSuspendAfterOneMonthPolicy implementa SuspendingPolicyInterface {función pública suspender(Suscripción $suscripción, DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
Estado de clase final {const privado EXPIRED = 'caducado'; const privado ACTIVO = 'activo'; const privado NUEVO = 'nuevo'; const privado SUSPENDIDO = 'suspendido'; función privada __construct(cadena privada de solo lectura $estado) {$este->estado = $estado; }función estática pública caducada(): self{return new self(self::EXPIRED); }función estática pública activa(): self{return new self(self::ACTIVE); }función estática pública nueva(): self{return new self(self::NEW); }función estática pública suspendida(): self{return new self(self::SUSPENDED); }función pública esEqual(self $estado): bool{return $this->status === $status->status; } }
Suscripción clase final {Estado privado $status;DateTimeImmutable $createdAt privado;función pública __construct(DateTimeImmutable $createdAt) {$this->status = Estado::new();$this->createdAt = $createdAt; }función pública suspender(SuspendingPolicyInterface $suspendingPolicy, DateTimeImmutable $at): bool{$resultado = $suspendingPolicy->suspend($this, $at);if ($resultado) {$this->status = Status::suspended() ; }retornar $resultado; }la función pública esOlderThan(DateTimeImmutable $date): bool{return $this->createdAt < $date; }función pública activar(): void{$this->status = Estado::activo(); }función pública expirar(): void{$this->status = Estado::expirado(); }función pública isExpired(): bool{return $this->status->isEqual(Status::expired()); }función pública isActive(): bool{return $this->status->isEqual(Status::active()); }función pública esNuevo(): bool{return $this->estado->isEqual(Estado::nuevo()); }función pública isSuspended(): bool{return $this->status->isEqual(Status::suspended()); } }
La clase final SubscriptionSuspendingTest extiende TestCase {/** * @test */función pública suspender_an_expired_subscription_with_cannot_suspend_expired_policy_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$sut->expire();$resultado = $sut ->suspender(nueva CannotSuspendExpiredSubscriptionPolicy(), nueva DateTimeImmutable());self::assertFalse($resultado); }/** * @test */función pública suspensing_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable());$resultado = $sut->suspend(new CannotSuspendNewSubscriptionPolicy(), new DateTimeImmutable());self ::assertFalse($resultado); }/** * @test */función pública suspensing_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$resultado = $sut->suspend(new CannotSuspendNewSubscriptionPolicy() , nuevo DateTimeImmutable());self::assertTrue($resultado); }/** * @test */función pública suspender_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void{$sut = nueva suscripción(nueva DateTimeImmutable());$sut->activate();$resultado = $sut->suspend(nueva CannotSuspendExpiredSubscriptionPolicy() , nuevo DateTimeImmutable());self::assertTrue($resultado); }/** * @test */función pública suspender_an_subscription_before_a_one_month_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable('2020-01-01'));$resultado = $sut->suspend(new CanSuspendAfterOneMonthPolicy(), nuevo DateTimeImmutable('2020-01-10'));self::assertFalse($resultado); }/** * @test */función pública suspender_an_subscription_after_a_one_month_is_possible(): void{$sut = nueva suscripción(new DateTimeImmutable('2020-01-01'));$resultado = $sut->suspend(new CanSuspendAfterOneMonthPolicy(), nuevo DateTimeImmutable('2020-02-02'));self::assertTrue($resultado); } }
¿Cómo realizar una prueba unitaria adecuada de una clase como esta?
servicio de aplicación de clase {función pública __construct(Repositorio de pedidos privado de solo lectura $Repositorio de pedidos,Repositorio de formularios privado de solo lectura $formRepository) {}función pública changeFormStatus(int $orderId): void{$order = $this->orderRepository->getById($orderId);$soapResponse = $ esto->getSoapClient()->getStatusByOrderId($orderId);$formulario = $this->formRepository->getByOrderId($orderId);$form->setStatus($soapResponse['status']);$form->setModifiedAt(new DateTimeImmutable());if ($soapResponse['status'] = == 'aceptado') {$pedido->setStatus('pagado'); }$this->formRepository->save($formulario);$this->orderRepository->save($order); }función privada getSoapClient(): SoapClient{return new SoapClient('https://legacy_system.pl/Soap/WebService', []); } }
[!TIP|estilo:plano|etiqueta:BUENO]
Es necesario dividir un código demasiado complicado en clases separadas.
Servicio de aplicación de clase final {función pública __construct(OrderRepositoryInterface de solo lectura privada $orderRepository,FormRepositoryInterface de solo lectura privada $formRepository,FormApiInterface de solo lectura privada $formApi,ChangeFormStatusService de solo lectura privada $changeFormStatusService) {}función pública changeFormStatus(int $orderId): void{$order = $this->orderRepository ->getById($orderId);$formulario = $this->formRepository->getByOrderId($orderId);$status = $this->formApi->getStatusByOrderId($orderId);$this->changeFormStatusService->changeStatus($order, $form, $status);$this ->formRepository->save($formulario);$this->orderRepository->save($order); } }
clase final ChangeFormStatusService {función pública changeStatus(Orden $orden, Formulario $form, cadena $formStatus): void{$status = FormStatus::createFromString($formStatus);$form->changeStatus($status);if ($form->isAccepted( )) {$pedido->cambiarEstado(EstadodelPedido::pagado()); } } }
la clase final ChangingFormStatusTest extiende TestCase {/** * @test */función pública cambiando_a_form_status_to_accepted_changes_an_order_status_to_paid(): void{$order = nuevo pedido();$form = nuevo formulario();$status = 'aceptado';$sut = nuevo ChangeFormStatusService();$sut ->cambiarEstado($pedido, $formulario, $estado);self::assertTrue($formulario->isAccepted());self::assertTrue($orden->isPaid()); }/** * @test */función pública cambiando_a_form_status_to_refused_not_changes_an_order_status(): void{$order = nuevo pedido();$form = nuevo formulario();$status = 'nuevo';$sut = nuevo ChangeFormStatusService();$sut ->cambiarEstado($pedido, $formulario, $estado);self::assertFalse($formulario->isAccepted());self::assertFalse($pedido->isPaid()); } }
Sin embargo, ApplicationService probablemente debería probarse mediante una prueba de integración solo con FormApiInterface simulada.
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
Cliente de clase final {función pública __construct(cadena privada $nombre) {}función pública getName(): cadena{return $this->nombre; }función pública setName(cadena $nombre): void{$this->nombre = $nombre; } }
La clase final CustomerTest extiende TestCase {función pública testSetName(): void{$cliente = new Cliente('Jack');$cliente->setName('John');self::assertSame('John', $cliente->getName()); } }
EventSubscriber de clase final {función estática pública getSubscribedEvents(): matriz{return ['event' => 'onEvent']; }función pública onEvent(): nulo{ } }
la clase final EventSubscriberTest extiende TestCase {función pública testGetSubscribedEvents(): void{$resultado = EventSubscriber::getSubscribedEvents();self::assertSame(['event' => 'onEvent'], $resultado); } }
[!ATENCIÓN] Probar el código sin ninguna lógica complicada no tiene sentido, pero también genera pruebas frágiles.
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
Repositorio de usuarios de clase final {función pública __construct(Conexión privada de solo lectura $conexión) {}función pública getUserNameByEmail(cadena $correo electrónico): ?array{return $this->conexión->createQueryBuilder() ->desde('usuario', 'u') ->dónde('u.correo electrónico =:correo electrónico') ->setParameter('correo electrónico', $correo electrónico) ->ejecutar() ->buscar(); } }
La clase final TestUserRepository extiende TestCase {función pública testGetUserNameByEmail(): void{$email = '[email protected]';$conexión = $this->createMock(Connection::class);$queryBuilder = $this->createMock(QueryBuilder::class); $resultado = $this->createMock(ResultStatement::class);$userRepository = nuevo UserRepository($conexión);$conexión->espera($this->once()) ->método('crearQueryBuilder') ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->método('de') ->con('usuario', 'u') ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->método('dónde') ->with('u.correo electrónico =:correo electrónico') ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->método('establecerParámetro') ->con('correo electrónico', $correo electrónico) ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->método('ejecutar') -> willreturn ($ resultado); $ resultado-> espera ($ this-> una vez ()) -> Método ('Fetch') -> willreturn (['correo electrónico' => $ correo electrónico]); $ resultado = $ userRepository-> getUsernameByEmail ($ correo electrónico); self :: afirmoSame (['email' => $ correo electrónico], $ result); } }
[!ATENCIÓN] Probar repositorios de esa manera genera pruebas frágiles y luego la refactorización es difícil. Para probar repositorios, escriba pruebas de integración.
[!TIP|estilo:plano|etiqueta:BUENO]
La clase final Goodtest extiende la prueba de prueba {private suscriptionFactory $ sut; public function setup (): void {$ this-> sut = new suscriptionFactory (); }/ */** * @test */public function crees_a_subscription_for_a_given_date_range (): void {$ result = $ this-> Sut-> Create (nuevo DatetimeMtable (), nuevo DatetimeMtable ('ahora +1 año'); (Suscripción :: clase, $ resultado); }/** * @Test */public Function Shows_an_exception_on_invalid_date_range (): void {$ this-> wupeException (createSubscriptionException :: class); $ result = $ this-> sut-> create (nuevo DatetimeMtable ('ahora -1 año'), nuevo DatetimeMutable ()); } }
Nota
El mejor caso para utilizar el método setUp será probar objetos sin estado.
Cualquier configuración realizada dentro de setUp
combina las pruebas y tiene un impacto en todas las pruebas.
Es mejor evitar un estado compartido entre pruebas y configurar el estado inicial de acuerdo con el método de prueba.
La legibilidad es peor en comparación con la configuración realizada con el método de prueba adecuado.
[!TIP|estilo:plano|etiqueta:MEJOR]
La clase final BetterTest extiende la prueba de prueba {/** * @test */public function suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible (): void {$ sut = $ this-> createAnActiveSubscription (); $ resultado = $ sut-> STSVEND (newOtsUspendNeWSubscriptionPolicy (), nieweTime (); : afirmarTrue ($ resultado); }/** * @test */public function suspending_an_active_subscription_with_cannot_suspend_exptired_policy_is_possible (): void {$ sut = $ this-> createAnactiveSubscription (); $ resultado = $ sut-> STSVEND (newOtSUspendExtExpiExpiredSubscriptionPolicy (), neweTimeMutTime; : afirmarTrue ($ resultado); }/** * @test */public function suspending_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible (): void {$ sut = $ this-> createSeWSubscription (); $ resultado = $ sut-> Suspend (Suspend (NewnnotsUspendNeWSubscriptionPscription (), NeweTime (); : afirmarfalse ($ resultado); } Función privada CreateAsewsubScription (): suscripción {return nueva suscripción (new DateTimeMutable ()); } Función privada CreateAnActivesUsCcription (): suscripción {$ suscripción = nueva suscripción (new DateTimeMutable ()); $ suscripción-> activate (); devolver $ suscripción; } }
Nota
Este enfoque mejora la legibilidad y aclara la separación (el código se lee más que se escribe).
Los ayudantes privados pueden resultar tediosos de utilizar en cada método de prueba, aunque proporcionan intenciones explícitas.
Para compartir objetos de prueba similares entre varias clases de prueba, utilice:
madre objeto
Constructor
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
Cliente de clase final {Private CustomerType $ type; privado descuento de descuento para la entrada {$ this-> type = customerType :: normal (); $ this-> scallcalcatulationPolicy = new NormalDiscountPolicy (); } función pública MakeVIP (): void {$ this-> type = customerType :: vip (); $ this-> descuidtcalcalpolicy = new vipDiscountPolicy (); } función pública getCustomerType (): CustomerType {return $ this-> type; } función pública getPermentEdisEdiscount (): int {return $ this-> scATECTCALCULATIONSPOLICY-> getPORTENDEGEGEDISPOUNT (); } }
Class Final InvalidTest extiende TestCase {Public Function testMakEvip (): void {$ sut = new Customer (); $ Sut-> MakEvip (); self :: afirmanSame (CustomerType :: vip (), $ sut-> getCustomerType ()); } }
[!TIP|estilo:plano|etiqueta:BUENO]
Cliente de clase final {Private CustomerType $ type; privado descuento de descuento para la entrada {$ this-> type = customerType :: normal (); $ this-> scallcalcatulationPolicy = new NormalDiscountPolicy (); } función pública MakeVIP (): void {$ this-> type = customerType :: vip (); $ this-> descuidtcalcalpolicy = new vipDiscountPolicy (); } función pública getPermentEdisEdiscount (): int {return $ this-> scATECTCALCULATIONSPOLICY-> getPORTENDEGEGEDISPOUNT (); } }
La validación de la clase final extiende la prueba de prueba {/** * @test */public function a_vip_customer_has_a_25_percentage_discount (): void {$ sut = new Customer (); $ sut-> makevip (); self :: afirman (25, $ sut-> getPercentegedEdageunt ()); } }
[!ATTENTION] Agregar código de producción adicional (por ejemplo, getter getCustomerType()) solo para verificar el estado en las pruebas es una mala práctica. Debe ser verificado por otro valor significativo del dominio (en este caso getPercentageDiscount()). Por supuesto, a veces puede resultar difícil encontrar otra forma de verificar el funcionamiento y podemos vernos obligados a agregar código de producción adicional para verificar la corrección en las pruebas, pero debemos intentar evitarlo.
Clase final Descuadercalculator {Public Function Calcule (int $ isvipfromyears): int { Afirmar :: greatthaneq ($ isvipfromyears, 0); return min (($ isvipfromyears * 10) + 3, 80); } }
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
Class Final InvalidTest extiende TestCase {/** * @dataprovider DiscountDataProvider */public Function testCalCalation (int $ vipdaysFrom, int $ esperado): void {$ sut = new DiscutorCalculator (); self :: afirmación ($ esperado, $ sut-> calcule ($ vipaysfrom) ); } function public scETTDATAPROVIDER (): Array {return [ [0, 0 * 10 + 3], // Detalles del dominio con fugas [1, 1 * 10 + 3], [5, 5 * 10 + 3], [8, 80] ]; } }
[!TIP|estilo:plano|etiqueta:BUENO]
La validación de la clase final extiende la prueba de prueba {/** * @dataprovider DiscountDataProvider */public Function testCalCalation (int $ vipdaysFrom, int $ esperado): void {$ sut = new DiscutorCalculator (); self :: afirmación ($ esperado, $ sut-> calcule ($ vipaysfrom) ); } function public scETTDATAPROVIDER (): Array {return [ [0, 3], [1, 13], [5, 53], [8, 80] ]; } }
Nota
No duplique la lógica de producción en las pruebas. Simplemente verifique los resultados mediante valores codificados.
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
clase de descuento {Función pública CalculeInternDiscount (int $ isvipfromyears): int { Afirmar :: greatthaneq ($ isvipfromyears, 0); return min (($ isvipfromyears * 10) + 3, 80); } Función pública CalculeAdDitionDiscountFromExternalSystem (): int {// Obtener datos de un sistema externo para calcular un DiscoUntreTurn 5; } }
Class OrderService {Función pública __Construct (Private Readonly DescuenceCalculator $ DiscutorCalculator) {} Función pública getToTalPriceWithDiscount (int $ TotalPrice, int $ VIPFromays): int {$ InternalDiscount = $ this-> descuento-> calculate -interdiscount ($ VIPFromays); $ this-> descuentocalcule-> calculeAdditionDiscountFromExternalSystem (); $ descuento = $ internalDiscount + $ externaldiscount; return $ totalprice-(int) techo (($ totalprice * $ descuento) / 100); } }
Class Final InvalidTest extiende TestCase {/** * @Dataprovider OrderDataprovider */public Function testGetToTalPriceWithDiscount (int $ TotalPrice, int $ vipdaysFrom, int $ esperado): void {$ descuidtcalculator = $ this-> createPartialMock (DiscountCalculator :: class ,: class,, ['CalculeAdDitionDiscountFromExternalSystem']); $ descuidado Método-> , $ VIPDAYSFROM)); } Public Function OrderDataprovider (): Array {return [ [1000, 0, 920], [500, 1, 410], [644, 5, 270], ]; } }
[!TIP|estilo:plano|etiqueta:BUENO]
interfaz externaldiscountCalculatorInterface {Public Function Calcule (): int; }
Clase final InternalDiscountCalculator {Public Function Calcule (int $ isvipfromyears): int { Afirmar :: greatthaneq ($ isvipfromyears, 0); return min (($ isvipfromyears * 10) + 3, 80); } }
Final Clase OrderService {Función pública __Construct (private Readonly InternalDiscountCalculator $ DISTRECTCALCULATULA, Private ReadOnly ExternisDisDiscountCalculatorInterface $ externo ExternisDiscountCalculator) {} función pública getToTalPriceWithDiscount (int $ totalPrice, int $ VIPFromdays): int {$ InternalDiscount = $-thantcalculation-> calcule ($ VIPFromdays): $ externaldiscount = $ this-> externoDiscountCalculator-> calculado (); $ descuento = $ internalDiscount + $ externalDiscount; return $ totalprice-(int) tceil (($ totalPrice * $ descuento) / 100); } }
La validación de la clase final extiende la prueba de prueba {/** * @dataprovider OrderDataprovider */public Function testGetToTalPriceWithDiscount (int $ TotalPrice, int $ vipdaysFrom, int $ esperado): void {$ externalDiscountCalculator = new class () implementa ExternisDiscountCalInterface {Función pública calculada (): int {regresh 5; } }; $ sut = new OrderService (new InternalDiscountCalculator (), $ externalDiscountCalculator); self :: afirmSeMe ($ esperado, $ sut-> getTotalPriceWithDiscount ($ totalpricio, $ vipdaysfrom)); } Public Function OrderDataprovider (): Array {return [ [1000, 0, 920], [500, 1, 410], [644, 5, 270], ]; } }
Nota
La necesidad de burlarse de una clase concreta para reemplazar una parte de su comportamiento significa que esta clase probablemente sea demasiado complicada y viole el Principio de Responsabilidad Única.
Orden de clase final de clase {Función pública __construct (public Readonly int $ Total) {} }
Orden de clase final {/** * @param ordenitem [] $ elementos * @param int $ transportcost */public function __construct (private array $ items, private int $ transportcost) {} function public getTotal (): int {return $ this-> getItemstotal () + $ this-> transportcost; } función privada getItemStotal (): int {return array_reduce (array_map (fn (ordenitem $ item) => $ item-> total, $ this-> elementos), fn (int $ sum, int $ total) => $ sum + = $ total, 0); } }
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
Class Final InvalidTest extiende TestCase {/** * @test * @dataprovider OrdersDataprovider */public Function get_total_returns_a_total_cost_of_a_whole_order (orden $ orden, int $ esperado): void {self :: afirmación ($ esperado, $ orden-> getTotal ()); }/** * @test * @dataprovider OrderItemsDataprovider */public Function get_items_total_returns_a_total_cost_of_all_items (orden $ orden, int $ esperado): void {self :: afirmación ($ esperadoTotal, $ this-> invokePrivateMethetTetEttetal) ($ orden) ($ ordene ($ ordene) ($ orden) ($ ordene) ($ orden) ($ orden) ($ orden) ($ orden) ($ orden); } Public Function OrdersDataprovider (): Array {return [ [Nuevo pedido ([New OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 75], [Nuevo pedido ([New OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [Nuevo pedido ([New OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 306] ]; } Public Function OrderItemSDATAPROVider (): Array {return [ [Nuevo pedido ([New OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 60], [Nuevo pedido ([New OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [Nuevo pedido ([New OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 297] ]; } función privada InvokePrivateMethodgetItemStotal (Order & $ Order): int {$ Reflection = new ReflectionClass (get_class ($ orden); $ Method = $ Reflection-> getMethod ('getItemStotal'); $ Method-> setAccessible (true); regreso; $ Method-> Invokeargs ($ orden, []); } }
[!TIP|estilo:plano|etiqueta:BUENO]
La validación de la clase final extiende la prueba de prueba {/** * @test * @dataprovider OrdersDataprovider */public Function get_total_returns_a_total_cost_of_a_whole_order (orden $ orden, int $ esperado): void {self :: afirmación ($ esperado, $ orden-> getTotal ()); } Public Function OrdersDataprovider (): Array {return [ [Nuevo pedido ([New OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 75], [Nuevo pedido ([New OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [Nuevo pedido ([New OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 306] ]; } }
[!ATTENTION] Las pruebas solo deben verificar la API pública.
El tiempo es una dependencia volátil porque no es determinista. Cada invocación devuelve un resultado diferente.
[!ADVERTENCIA|estilo:plano|etiqueta:MALO]
Reloj de clase final {public static dateTime | null $ currentDateTime = null; public static función getCurrentDateTime (): dateTime {if (null === self :: $ currentDateTime) {self :: $ currentDateTime = new DateTime (); } return self :: $ currentDateTime; } conjunto de funciones estáticas públicas (DateTime $ DateTime): void {self :: $ currentDateTime = $ dateTime; } Función estática pública RESET (): void {self :: $ currentDateTime = null; } }
Cliente de clase final {Private DateTime $ creatingat; publicidad pública __construct () {$ this-> creating = clock :: getCurrentDateTime (); } función pública isvip (): bool {return $ this-> createat-> diff (clock :: getCurrentDateTime ())-> y> = 1; } }
Class Final InvalidTest extiende TestCase {/** * @test */public function a_customer_registered_more_than_a_one_year_ago_is_a_vip (): void { Clock :: set (nuevo DateTime ('2019-01-01')); $ Sut = new Customer (); Reloj :: reset (); // debe recordar al restablecer los estados compartidos :: afirmartrue ($ sut-> isvip ()); }/** * @test */public function a_customer_registered_less_than_a_one_year_ago_is_not_a_vip (): void { Clock :: set ((nuevo DateTime ())-> Sub (nuevo DateInterval ('P2M'))); $ Sut = New Customer (); Reloj :: reset (); // debe recordar al restablecer los estados compartidos :: afirmarfalse ($ sut-> isvip ()); } }
[!TIP|estilo:plano|etiqueta:BUENO]
Interface ClockInterface {Función pública getCurrentTime (): DateTimeMutable; }
Clock final de clase final implementa ClockInterface {función privada __construct () { } Función estática pública create (): self {return new self (); } función pública getCurrentTime (): DateTimeMutable {return New DateTimeMutable (); } }
La clase final FixedClock implementa ClockInterface {Función privada __construct (Private Readonly DateTimeMutable $ FixedDate) {} public static función create (DateTimeMutable $ fixeddate): self {return new self ($ fixeddate); } función pública getCurrentTime (): DateTimeMutable {return $ this-> fixeddate; } }
Cliente de clase final {Función pública __Construct (private Readonly DateTimeMutable $ creatingat) {} Función pública ISVIP (DateTimeMtable $ CurrentDate): bool {return $ this-> creatat-> diff ($ currentDate)-> y> = 1; } }
La validación de la clase final extiende la prueba de prueba {/** * @test */public function a_customer_registered_more_than_a_one_year_ago_is_a_vip (): void {$ sut = nuevo cliente (fixlock :: create (nuevo (nuevo DateTimeMtable ('2019-01-01'))-> getCurrentTime ()); self :: afirTtrue ($ sut-> isvip (fixlock :: create (nuevo DatetimeMtable ('2020-01-02'))-> getCurrentTime ( ))); }/** * @test */public function a_customer_registered_less_than_a_one_year_ago_is_not_a_vip (): void {$ sut = new Customer (FixedClock :: Create (nuevo (nuevo CREA DateTimeMtable ('2019-01-01'))-> getCurrentTime ()); self :: afirmofalse ($ sut-> isvip (fixlock :: create (nuevo DateTimeMtable ('2019-05-02'))-> GetCurrentTime ( ))); } }
Nota
La hora y los números aleatorios no deben generarse directamente en el código de dominio. Para probar el comportamiento debemos tener resultados deterministas, por lo que debemos inyectar estos valores en un objeto de dominio como en el ejemplo anterior.
La cobertura del 100% no es el objetivo o incluso no es deseable porque si hay una cobertura del 100%, las pruebas probablemente serán muy frágiles, lo que significa que la refactorización será muy difícil. Las pruebas de mutaciones brindan una mejor información sobre la calidad de las pruebas. Leer más
Desarrollo impulsado por las pruebas: por ejemplo / Kent Beck - The Classic
Principios, prácticas y patrones de pruebas unitarias / Vladimir Khorikov: el mejor libro sobre pruebas que he leído
Kamil Ruczyński
Gorjeo: https://twitter.com/Sarvendev
Blog: https://sarvendev.com/
LinkedIn: https://www.linkedin.com/in/kamilruczynski/