In diesen Zeiten sind die Vorteile des Schreibens von Unit-Tests enorm. Ich denke, dass die meisten der kürzlich gestarteten Projekte irgendwelche Unit-Tests enthalten. In Unternehmensanwendungen mit viel Geschäftslogik sind Unit-Tests die wichtigsten Tests, da sie schnell sind und wir sofort sicherstellen können, dass unsere Implementierung korrekt ist. Allerdings sehe ich oft ein Problem mit guten Tests in Projekten, obwohl der Nutzen dieser Tests nur dann enorm ist, wenn man über gute Unit-Tests verfügt. In diesen Beispielen werde ich versuchen, einige Tipps zu geben, wie man gute Unit-Tests schreibt.
Leicht lesbare Version: https://testing-tips.sarvendev.com/
Kamil Ruczyński
Blog: https://sarvendev.com/
LinkedIn: https://www.linkedin.com/in/kamilruczynski/
Ihre Unterstützung bedeutet mir sehr viel! Wenn Ihnen dieser Leitfaden gefallen hat und Sie Wert auf das geteilte Wissen legen, denken Sie darüber nach, mich auf BuyMeCoffee zu unterstützen:
oder hinterlassen Sie einfach einen Stern im Repository und folgen Sie mir auf Twitter und Github, um über alle Updates auf dem Laufenden zu bleiben. Ihre Großzügigkeit beflügelt meine Leidenschaft, aufschlussreichere Inhalte für Sie zu erstellen.
Wenn Sie Verbesserungsideen oder ein Thema haben, über das Sie schreiben möchten, bereiten Sie gerne eine Pull-Anfrage vor oder lassen Sie es mich einfach wissen.
Abonnieren Sie und meistern Sie Unit-Tests mit meinem KOSTENLOSEN eBook!
Einzelheiten
Ich habe noch eine ziemlich lange TODO-Liste mit Verbesserungen an diesem Leitfaden zum Thema Unit Testing und werde sie in naher Zukunft vorstellen.
Einführung
Autor
Test verdoppelt
Benennung
AAA-Muster
Objektmutter
Baumeister
Objekt beanspruchen
Parametrisierter Test
Zwei Schulen für Unit-Tests
Klassisch
Spötter
Abhängigkeiten
Mock vs. Stub
Drei Arten von Unit-Tests
Ausgabe
Zustand
Kommunikation
Funktionale Architektur und Tests
Beobachtbares Verhalten vs. Implementierungsdetails
Verhaltenseinheit
Bescheidenes Muster
Trivialer Test
Zerbrechlicher Test
Testvorrichtungen
Allgemeines Testen von Anti-Patterns
Privatstaat bloßstellen
Offenlegung von Domänendetails
Konkrete Klassen verspotten
Testen privater Methoden
Zeit als volatile Abhängigkeit
Eine 100-prozentige Testabdeckung sollte nicht das Ziel sein
Empfohlene Bücher
Testdoppelungen sind gefälschte Abhängigkeiten, die in Tests verwendet werden.
Ein Dummy ist eine einfache Implementierung, die nichts bewirkt.
Die letzte Klasse Mailer implementiert MailerInterface {öffentliche Funktion send(Message $message): void{ } }
Ein Fake ist eine vereinfachte Implementierung zur Simulation des ursprünglichen Verhaltens.
Die letzte Klasse InMemoryCustomerRepository implementiert CustomerRepositoryInterface {/** * @var Customer[] */private array $customers;public function __construct() {$this->customers = []; }public function store(Customer $customer): void{$this->customers[(string) $customer->id()->id()] = $customer; }public function get(CustomerId $id): Customer{if (!isset($this->customers[(string) $id->id()])) {throw new CustomerNotFoundException(); }return $this->customers[(string) $id->id()]; }public function findByEmail(Email $email): Customer{foreach ($this->customers as $customer) {if ($customer->getEmail()->isEqual($email)) {return $customer; } }throw new CustomerNotFoundException(); } }
Ein Stub ist die einfachste Implementierung mit fest codiertem Verhalten.
Die letzte Klasse UniqueEmailSpecificationStub implementiert UniqueEmailSpecificationInterface {public function isUnique(Email $email): bool{return true; } }
$specificationStub = $this->createStub(UniqueEmailSpecificationInterface::class);$specificationStub->method('isUnique')->willReturn(true);
Ein Spion ist eine Implementierung zur Überprüfung eines bestimmten Verhaltens.
Die letzte Klasse Mailer implementiert MailerInterface {/** * @var Message[] */private array $messages; öffentliche Funktion __construct() {$this->messages = []; }public function send(Message $message): void{$this->messages[] = $message; }public function getCountOfSentMessages(): int{return count($this->messages); } }
Ein Mock ist eine konfigurierte Nachahmung zur Überprüfung von Anrufen eines Mitarbeiters.
$message = new Message('[email protected]', 'Test', 'Test test test');$mailer = $this->createMock(MailerInterface::class);$mailer->expects($this-> einmal()) ->Methode('senden') ->with($this->equalTo($message));
[!ATTENTION] Um eingehende Interaktionen zu überprüfen, verwenden Sie einen Stub, aber um ausgehende Interaktionen zu überprüfen, verwenden Sie einen Mock.
Mehr: Mock vs Stub
[!WARNING|style:flat|label:NICHT GUT]
Die letzte Klasse TestExample erweitert TestCase {/** * @test */public function sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->method('getAll')->willReturn([$message1, $message2]);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('send') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|style:flat|label:BETTER]
Besserer Widerstand gegen Refactoring
Die Verwendung von Refactor->Rename für die jeweilige Methode unterbricht den Test nicht
Bessere Lesbarkeit
Geringere Wartungskosten
Es ist nicht erforderlich, diese anspruchsvollen Mock-Frameworks zu erlernen
Nur einfacher PHP-Code
Die letzte Klasse TestExample erweitert TestCase {/** * @test */public function 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([$message1, $message2]); } }
[!WARNING|style:flat|label:NICHT GUT]
öffentliche Funktion test(): void{$subscription = SubscriptionMother::new();$subscription->activate();self::assertSame(Status::activated(), $subscription->status()); }
[!TIP|style:flat|label:Geben Sie explizit an, was Sie testen]
öffentliche Funktion sut(): void{// sut = System im Test$sut = SubscriptionMother::new();$sut->activate();self::assertSame(Status::activated(), $sut->status ()); }
[!WARNING|style:flat|label:NICHT GUT]
öffentliche Funktion it_throws_invalid_credentials_Exception_when_sign_in_with_invalid_credentials(): void{ }öffentliche Funktion testCreatingWithATooShortPasswordIsNotPossible(): void{ }öffentliche Funktion testDeactivateASubscription(): void{ }
[!TIP|style:flat|label:BETTER]
Die Verwendung von Unterstrichen verbessert die Lesbarkeit
Der Name sollte das Verhalten beschreiben, nicht die Implementierung
Verwenden Sie Namen ohne technische Schlüsselwörter. Es sollte für eine Person, die kein Programmierer ist, lesbar sein.
öffentliche Funktion sign_in_with_invalid_credentials_is_not_possible(): void{ }öffentliche Funktion Creating_with_a_too_short_password_is_not_possible(): void{ }öffentliche Funktion deactivating_an_activated_subscription_is_valid(): void{ }öffentliche Funktion deactivating_an_inactive_subscription_is_invalid(): void{ }
Notiz
Die Beschreibung des Verhaltens ist beim Testen der Domänenszenarien wichtig. Wenn es sich bei Ihrem Code nur um einen Dienstprogrammcode handelt, ist er weniger wichtig.
Warum sollte es für einen Nicht-Programmierer nützlich sein, Unit-Tests zu lesen?
Wenn es sich um ein Projekt mit komplexer Domänenlogik handelt, muss diese Logik für alle sehr klar sein, sodass Tests Domänendetails ohne technische Schlüsselwörter beschreiben und Sie mit einem Unternehmen in einer Sprache wie in diesen Tests sprechen können. Der gesamte Code, der sich auf die Domain bezieht, sollte frei von technischen Details sein. Ein Nicht-Programmierer wird diese Tests nicht lesen können. Wenn Sie über die Domäne sprechen möchten, sind diese Tests hilfreich, um zu erfahren, was diese Domäne tut. Es wird eine Beschreibung ohne technische Details geben, z. B. gibt Null zurück, löst eine Ausnahme aus usw. Diese Art von Informationen hat nichts mit der Domain zu tun, daher sollten wir diese Schlüsselwörter nicht verwenden.
Es ist auch üblich: Gegeben, wann, dann.
Trennen Sie den Test in drei Abschnitte:
Anordnen : Bringen Sie das zu testende System in den gewünschten Zustand. Bereiten Sie Abhängigkeiten und Argumente vor und erstellen Sie schließlich das SUT.
Handlung : Rufen Sie ein getestetes Element auf.
Bestätigen : Überprüfen Sie das Ergebnis, den Endzustand oder die Kommunikation mit Mitarbeitern.
[!TIP|style:flat|label:GOOD]
öffentliche Funktion aaa_pattern_example_test(): void{//Arrange|Given$sut = SubscriptionMother::new();//Act|When$sut->activate();//Assert|Thenself::assertSame(Status::activated( ), $sut->status()); }
Das Muster hilft dabei, spezifische Objekte zu erstellen, die in einigen Tests wiederverwendet werden können. Aus diesem Grund ist der Abschnitt zur Anordnung prägnanter und der Test insgesamt besser lesbar.
Abschlussklasse AbonnementMutter {public static function new(): Subscription{return new Subscription(); }public static function activate(): Subscription{$subscription = new Subscription();$subscription->activate();return $subscription; }öffentliche statische Funktion deaktiviert(): Subscription{$subscription = self::activated();$subscription->deactivate();return $subscription; } }
letzte Klasse Beispieltest {public function example_test_with_activated_subscription(): void{$activatedSubscription = SubscriptionMother::activated();// etwas tun// etwas überprüfen}public function example_test_with_deactivated_subscription(): void{$deactivatedSubscription = SubscriptionMother::deactivated();// etwas tun // etwas überprüfen} }
Builder ist ein weiteres Muster, das uns hilft, Objekte in Tests zu erstellen. Im Vergleich zu Object Mother eignet sich Pattern Builder besser zum Erstellen komplexerer Objekte.
letzte Klasse OrderBuilder {private DateTimeImmutable|null $createdAt = null;/** * @var OrderItem[] */private array $items = [];public function erstelltAt(DateTimeImmutable $createdAt): self{$this->createdAt = $createdAt;return $this; }public function withItem(string $name, int $price): self{$this->items[] = new OrderItem($name, $price);return $this; }public function build(): Order{ Assert::notEmpty($this->items);return new Order($this->createdAt ?? new DateTimeImmutable(),$this->items, ); } }
Die letzte Klasse „ExampleTest“ erweitert TestCase {/** * @test */public function example_test_with_order_builder(): void{$order = (new OrderBuilder()) ->createdAt(new DateTimeImmutable('2022-11-10 20:00:00')) ->withItem('Item 1', 1000) ->withItem('Item 2', 2000) ->withItem('Item 3', 3000) ->build();// etwas tun// etwas überprüfen} }
Das Assert-Objektmuster hilft dabei, besser lesbare Assert-Abschnitte zu schreiben. Anstatt ein paar Behauptungen zu verwenden, können wir einfach eine Abstraktion vorbereiten und natürliche Sprache verwenden, um zu beschreiben, welches Ergebnis erwartet wird.
Die letzte Klasse „ExampleTest“ erweitert TestCase {/** * @test */public function example_test_with_asserter(): void{$currentTime = new DateTimeImmutable('2022-11-10 20:00:00');$sut = new OrderService();$order = $sut ->create($currentTime); OrderAsserter::assertThat($order) ->wasCreatedAt($currentTime) ->hasTotal(6000); } }
Verwenden Sie PHPUnitFrameworkAssert;finale Klasse OrderAsserter {public function __construct(private readonly Order $order) {}public static function activateThat(Order $order): self{return new OrderAsserter($order); }public function wasCreatedAt(DateTimeImmutable $createdAt): self{ Assert::assertEquals($createdAt, $this->order->createdAt);return $this; }öffentliche Funktion hasTotal(int $total): self{ Assert::assertSame($total, $this->order->getTotal());return $this; } }
Der parametrisierte Test ist eine gute Option, um das SUT mit vielen Parametern zu testen, ohne den Code zu wiederholen.
Warnung
Diese Art von Test ist weniger lesbar. Um die Lesbarkeit etwas zu erhöhen, sollten Negativ- und Positivbeispiele auf verschiedene Tests aufgeteilt werden.
Die letzte Klasse „ExampleTest“ erweitert TestCase {/** * @test * @dataProvider getInvalidEmails */public function discovers_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( $result); }/** * @test * @dataProvider getValidEmails */public function discovers_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( $result); }public function getInvalidEmails(): iterable{yield 'Eine ungültige E-Mail ohne @' => ['test'];yield 'Eine ungültige E-Mail ohne die Domain nach @' => ['test@'];yield 'Eine ungültige E-Mail without TLD' => ['test@test'];//...}public function getValidEmails(): iterable{yield 'Eine gültige E-Mail mit Kleinbuchstaben' => ['[email protected]'];yield 'Eine gültige E-Mail mit Kleinbuchstaben und Ziffern' => ['[email protected]'];yield 'Eine gültige E-Mail mit Großbuchstaben und Ziffern' => ['Test123@ test.com'];//...} }
Notiz
Verwenden Sie yield
und fügen Sie den Fällen eine Textbeschreibung hinzu, um die Lesbarkeit zu verbessern.
Die Einheit ist eine einzelne Verhaltenseinheit, es können mehrere verwandte Klassen sein.
Jeder Test sollte von anderen isoliert werden. Daher muss es möglich sein, sie parallel oder in beliebiger Reihenfolge aufzurufen.
Die letzte Klasse TestExample erweitert TestCase {/** * @test */public function 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()); } }
Die Einheit ist eine einzelne Klasse.
Die Einheit sollte von allen Mitarbeitern isoliert sein.
Die letzte Klasse TestExample erweitert TestCase {/** * @test */public function suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = $this->createStub(SuspendingPolicyInterface::class);$canAlwaysSuspendPolicy->method('suspend')->willReturn(true);$ sut = new Subscription();$result = $sut->suspend($canAlwaysSuspendPolicy);self::assertTrue($result);self::assertSame(Status::suspend(), $sut->status()); } }
Notiz
Der klassische Ansatz ist besser, um fragile Tests zu vermeiden.
[TODO]
Beispiel:
letzte Klasse NotificationService {public function __construct(private readonly MailerInterface $mailer,private readonly MessageRepositoryInterface $messageRepository) {}public function send(): void{$messages = $this->messageRepository->getAll();foreach ($messages as $message) { $this->mailer->send($message); } } }
[!WARNING|style:flat|label:BAD]
Die Behauptung von Interaktionen mit Stubs führt zu fragilen Tests
Die letzte Klasse TestExample erweitert TestCase {/** * @test */public function sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->method('getAll')->willReturn([$message1, $message2]);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$messageRepository->expects(self::once())->method('getAll');$mailer- >expects(self::exactly(2))->method('send') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|style:flat|label:GOOD]
Die letzte Klasse TestExample erweitert TestCase {/** * @test */public function 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);// Assert-Interaktionen mit der stub$mailer->expects(self::exactly(2))->method('send') entfernt ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|style:flat|label:NOCH BESSER MIT SPY]
Die letzte Klasse TestExample erweitert TestCase {/** * @test */public function 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([$message1, $message2]); } }
[!TIP|style:flat|label:Die beste Option]
Der beste Widerstand gegen Refactoring
Beste Genauigkeit
Die niedrigsten Wartungskosten
Wenn möglich, sollten Sie diese Art von Test bevorzugen
Die letzte Klasse „ExampleTest“ erweitert TestCase {/** * @test * @dataProvider getInvalidEmails */public function discovers_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( $result); }/** * @test * @dataProvider getValidEmails */public function discovers_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( $result); }öffentliche Funktion getInvalidEmails(): array{return [ ['prüfen'], ['prüfen@'], ['test@test'],//...]; }öffentliche Funktion getValidEmails(): array{return [ ['[email protected]'], ['[email protected]'], ['[email protected]'],//...]; } }
[!WARNING|style:flat|label:Worse option]
Schlimmerer Widerstand gegen Refactoring
Schlechtere Genauigkeit
Höhere Wartungskosten
Die letzte Klasse „ExampleTest“ erweitert TestCase {/** * @test */public function Adding_an_item_to_cart(): void{$item = new CartItem('Product');$sut = new Cart();$sut->addItem($item);self::assertSame (1, $sut->getCount());self::assertSame($item, $sut->getItems()[0]); } }
[!ATTENTION|style:flat|label:Die schlechteste Option]
Der schlimmste Widerstand gegen Refactoring
Die schlechteste Genauigkeit
Die höchsten Kosten für Wartbarkeit
Die letzte Klasse „ExampleTest“ erweitert TestCase {/** * @test */public function 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('send') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!WARNING|style:flat|label:BAD]
letzte Klasse NameService {public function __construct(private readonly CacheStorageInterface $cacheStorage) {}public function loadAll(): void{$namesCsv = array_map('str_getcsv', file(__DIR__.'/../names.csv'));$names = [ ];foreach ($namesCsv as $nameData) {if (!isset($nameData[0], $nameData[1])) {continue; }$names[] = neuer Name($nameData[0], neues Geschlecht($nameData[1])); }$this->cacheStorage->store('names', $names); } }
Wie teste ich einen solchen Code? Dies ist nur mit einem Integrationstest möglich, da dieser direkt einen Infrastrukturcode verwendet, der sich auf ein Dateisystem bezieht.
[!TIP|style:flat|label:GOOD]
Wie in der funktionalen Architektur müssen wir einen Code mit Nebenwirkungen und Code, der nur Logik enthält, trennen.
letzte Klasse NameParser {/** * @param array<string[]> $namesData * @return Name[] */public function parse(array $namesData): array{$names = [];foreach ($namesData as $nameData) {if (!isset($nameData[0], $nameData[1])) {continue; }$names[] = neuer Name($nameData[0], neues Geschlecht($nameData[1])); }return $names; } }
letzte Klasse CsvNamesFileLoader {public function load(): array{return array_map('str_getcsv', file(__DIR__.'/../names.csv')); } }
Abschlussklasse ApplicationService {public function __construct(private readonly CsvNamesFileLoader $fileLoader,private readonly NameParser $parser,private readonly CacheStorageInterface $cacheStorage) {}public function loadNames(): void{$namesData = $this->fileLoader->load();$names = $this->parser->parse($namesData);$this->cacheStorage->store('names', $names); } }
Die letzte Klasse ValidUnitExampleTest erweitert TestCase {/** * @test */public function parse_all_names(): void{$namesData = [ ['John', 'M'], ['Lennon', 'U'], ['Sarah', 'W'] ];$sut = new NameParser();$result = $sut->parse($namesData); self::assertSame( [neuer Name('John', neues Geschlecht('M')),neuer Name('Lennon', neues Geschlecht('U')),neuer Name('Sarah', neues Geschlecht('W')) ],$result); } }
[!WARNING|style:flat|label:BAD]
Abschlussklasse ApplicationService {public function __construct(private readonly SubscriptionRepositoryInterface $subscriptionRepository) {}public function renewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);if (!$subscription->getStatus() ->isEqual(Status::expired())) {return false; }$subscription->setStatus(Status::active());$subscription->setModifiedAt(new DateTimeImmutable());return true; } }
Abonnement für den Abschlusskurs {public function __construct(private Status $status, private DateTimeImmutable $modifiedAt) {}public function getStatus(): Status{return $this->status; }public function setStatus(Status $status): void{$this->status = $status; }public function getModifiedAt(): DateTimeImmutable{return $this->modifiedAt; }public function setModifiedAt(DateTimeImmutable $modifiedAt): void{$this->modifiedAt = $modifiedAt; } }
Die letzte Klasse InvalidTestExample erweitert TestCase {/** * @test */public function renew_an_expired_subscription_is_possible(): void{$modifiedAt = new DateTimeImmutable();$expiredSubscription = new Subscription(Status::expired(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($expiredSubscription));$result = $sut->renewSubscription(1);self::assertSame(Status::active(), $expiredSubscription->getStatus());self::assertGreaterThan($modifiedAt, $expiredSubscription->getModifiedAt());self:: behauptenTrue($result); }/** * @test */public function renew_an_active_subscription_is_not_possible(): void{$modifiedAt = new DateTimeImmutable();$activeSubscription = new Subscription(Status::active(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($activeSubscription));$result = $sut->renewSubscription(1);self::assertSame($modifiedAt, $activeSubscription->getModifiedAt());self::assertFalse($result); } private Funktion createRepository(Subscription $subscription): SubscriptionRepositoryInterface{return new class ($expiredSubscription) implementiert SubscriptionRepositoryInterface {public function __construct(private readonly Subscription $subscription) {} öffentliche Funktion findById(int $id): Subscription{return $this->subscription; } }; } }
[!TIP|style:flat|label:GOOD]
Abschlussklasse ApplicationService {public function __construct(private readonly SubscriptionRepositoryInterface $subscriptionRepository) {}public function renewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);return $subscription->renew(new DateTimeImmutable( )); } }
Abonnement für den Abschlusskurs {private Status $status;private DateTimeImmutable $modifiedAt;public function __construct(DateTimeImmutable $modifiedAt) {$this->status = Status::new();$this->modifiedAt = $modifiedAt; }public function renew(DateTimeImmutable $modifiedAt): bool{if (!$this->status->isEqual(Status::expired())) {return false; }$this->status = Status::active();$this->modifiedAt = $modifiedAt;return true; }public function active(DateTimeImmutable $modifiedAt): void{//simplified$this->status = Status::active();$this->modifiedAt = $modifiedAt; }public function Expire(DateTimeImmutable $modifiedAt): void{//simplified$this->status = Status::expired();$this->modifiedAt = $modifiedAt; }public function isActive(): bool{return $this->status->isEqual(Status::active()); } }
Die letzte Klasse ValidTestExample erweitert TestCase {/** * @test */public function renew_an_expired_subscription_is_possible(): void{$expiredSubscription = SubscriptionMother::expired();$sut = new ApplicationService($this->createRepository($expiredSubscription));$result = $sut- >renewSubscription(1);// Überspringe die Überprüfung von „modifiedAt“, da es nicht Teil des beobachtbaren Verhaltens ist. Um diesen Wert zu überprüfen, müssten wir// einen Getter für ModifiedAt hinzufügen, wahrscheinlich nur zu Testzwecken.self::assertTrue($expiredSubscription->isActive());self::assertTrue($result); }/** * @test */public function renew_an_active_subscription_is_not_possible(): void{$activeSubscription = SubscriptionMother::active();$sut = new ApplicationService($this->createRepository($activeSubscription));$result = $sut- >renewSubscription(1);self::assertTrue($activeSubscription->isActive());self::assertFalse($result); } private Funktion createRepository(Subscription $subscription): SubscriptionRepositoryInterface{return new class ($expiredSubscription) implementiert SubscriptionRepositoryInterface {public function __construct(private readonly Subscription $subscription) {} öffentliche Funktion findById(int $id): Subscription{return $this->subscription; } }; } }
Notiz
Das erste Abo-Modell hat ein schlechtes Design. Um einen Geschäftsvorgang aufzurufen, müssen Sie drei Methoden aufrufen. Auch die Verwendung von Gettern zur Überprüfung des Betriebs ist keine gute Vorgehensweise. In diesem Fall wird die Überprüfung einer Änderung von modifiedAt
übersprungen. Das Festlegen eines bestimmten modifiedAt
während eines Erneuerungsvorgangs kann wahrscheinlich mit einem Ablaufgeschäftsvorgang getestet werden. Der Getter für modifiedAt
ist nicht erforderlich. Natürlich gibt es Fälle, in denen es sehr schwierig sein wird, die Möglichkeit zu finden, Getter zu vermeiden, die nur für Tests bereitgestellt werden, aber wir sollten immer versuchen, sie nicht einzuführen.
[!WARNING|style:flat|label:BAD]
Die Klasse CannotSuspendExpiredSubscriptionPolicy implementiert SuspendingPolicyInterface {public function suspend(Subscription $subscription, DateTimeImmutable $at): bool{if ($subscription->isExpired()) {return false; }return true; } }
Die Klasse CannotSuspendExpiredSubscriptionPolicyTest erweitert TestCase {/** * @test */public function it_returns_false_when_a_subscription_is_expired(): void{$policy = new CannotSuspendExpiredSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isExpired')->willReturn(true);self::assertFalse($policy->suspend($subscription, new DateTimeImmutable())); }/** * @test */public function 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())); } }
Die Klasse CannotSuspendNewSubscriptionPolicy implementiert SuspendingPolicyInterface {public function suspend(Subscription $subscription, DateTimeImmutable $at): bool{if ($subscription->isNew()) {return false; }return true; } }
Die Klasse CannotSuspendNewSubscriptionPolicyTest erweitert TestCase {/** * @test */public function it_returns_false_when_a_subscription_is_new(): void{$policy = new CannotSuspendNewSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isNew') ->willReturn(true);self::assertFalse($policy->suspend($subscription, new DateTimeImmutable())); }/** * @test */public function it_returns_true_when_a_subscription_is_not_new(): void{$policy = new CannotSuspendNewSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isNew') ->willReturn(false);self::assertTrue($policy->suspend($subscription, new DateTimeImmutable())); } }
Die Klasse CanSuspendAfterOneMonthPolicy implementiert SuspendingPolicyInterface {public function suspend(Subscription $subscription, DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
Die Klasse CanSuspendAfterOneMonthPolicyTest erweitert TestCase {/** * @test */public function it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable('2021-01-29');$policy = new CanSuspendAfterOneMonthPolicy();$subscription = new Subscription(new DateTimeImmutable('2020-12-28'));self::assertTrue($policy->suspend($subscription, $date)); }/** * @test */public function it_returns_false_when_a_subscription_is_not_older_than_one_month(): void{$date = new DateTimeImmutable('2021-01-29');$policy = new CanSuspendAfterOneMonthPolicy();$subscription = new Subscription(new DateTimeImmutable('2020-01-01'));self::assertTrue($policy->suspend($subscription, $date)); } }
Klassenstatus {private const EXPIRED = 'expired';private const ACTIVE = 'active';private const NEW = 'new';private const SUSPENDED = 'suspended';private function __construct(private readonly string $status) {$this->status = $status; }public static function Expired(): self{return new self(self::EXPIRED); }public static function active(): self{return new self(self::ACTIVE); }public static function new(): self{return new self(self::NEW); }öffentliche statische Funktion suspendiert(): self{return new self(self::SUSPENDED); }public function isEqual(self $status): bool{return $this->status === $status->status; } }
Die Klasse StatusTest erweitert TestCase {public function testEquals(): void{$status1 = Status::active();$status2 = Status::active();self::assertTrue($status1->isEqual($status2)); }public function testNotEquals(): void{$status1 = Status::active();$status2 = Status::expired();self::assertFalse($status1->isEqual($status2)); } }
Die Klasse SubscriptionTest erweitert TestCase {/** * @test */public function suspending_a_subscription_is_possible_when_a_policy_returns_true(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'suspend')->willReturn(true);$sut = new Subscription(new DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertTrue($result);self::assertTrue($sut->isSuspended()); }/** * @test */public function suspending_a_subscription_is_not_possible_when_a_policy_returns_false(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'suspend')->willReturn(false);$sut = new Subscription(new DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertFalse($result);self::assertFalse($sut->isSuspended()); }/** * @test */public function it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1M'));$sut = new Subscription($ date);self::assertTrue($sut->isOlderThan($futureDate)); }/** * @test */public function it_returns_false_when_a_subscription_is_not_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1D'));$sut = new Subscription($date);self::assertTrue($sut->isOlderThan($futureDate)); } }
[!ATTENTION] Schreiben Sie keinen Code 1:1, 1 Klasse: 1 Test. Dies führt zu fragilen Tests, die das Refactoring schwierig machen.
[!TIP|style:flat|label:GOOD]
Die letzte Klasse CannotSuspendExpiredSubscriptionPolicy implementiert SuspendingPolicyInterface {public function suspend(Subscription $subscription, DateTimeImmutable $at): bool{if ($subscription->isExpired()) {return false; }return true; } }
Die letzte Klasse CannotSuspendNewSubscriptionPolicy implementiert SuspendingPolicyInterface {public function suspend(Subscription $subscription, DateTimeImmutable $at): bool{if ($subscription->isNew()) {return false; }return true; } }
Die letzte Klasse CanSuspendAfterOneMonthPolicy implementiert SuspendingPolicyInterface {public function suspend(Subscription $subscription, DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
endgültiger Klassenstatus {private const EXPIRED = 'expired';private const ACTIVE = 'active';private const NEW = 'new';private const SUSPENDED = 'suspended';private function __construct(private readonly string $status) {$this->status = $status; }public static function Expired(): self{return new self(self::EXPIRED); }public static function active(): self{return new self(self::ACTIVE); }public static function new(): self{return new self(self::NEW); }öffentliche statische Funktion suspendiert(): self{return new self(self::SUSPENDED); }public function isEqual(self $status): bool{return $this->status === $status->status; } }
Abonnement für den Abschlusskurs {private Status $status;private DateTimeImmutable $createdAt;public function __construct(DateTimeImmutable $createdAt) {$this->status = Status::new();$this->createdAt = $createdAt; }public function suspend(SuspendingPolicyInterface $suspendingPolicy, DateTimeImmutable $at): bool{$result = $suspendingPolicy->suspend($this, $at);if ($result) {$this->status = Status::suspended() ; }return $result; }public function isOlderThan(DateTimeImmutable $date): bool{return $this->createdAt < $date; }public function activate(): void{$this->status = Status::active(); }public function Expire(): void{$this->status = Status::expired(); }public function isExpired(): bool{return $this->status->isEqual(Status::expired()); }public function isActive(): bool{return $this->status->isEqual(Status::active()); }public function isNew(): bool{return $this->status->isEqual(Status::new()); }public function isSuspended(): bool{return $this->status->isEqual(Status::suspended()); } }
Die letzte Klasse „SubscriptionSuspendingTest“ erweitert TestCase {/** * @test */public function suspending_an_expired_subscription_with_cannot_suspend_expired_policy_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$sut->expire();$result = $sut ->suspend(new CannotSuspendExpiredSubscriptionPolicy(), new DateTimeImmutable());self::assertFalse($result); }/** * @test */public function 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($result); }/** * @test */public function suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy() , new DateTimeImmutable());self::assertTrue($result); }/** * @test */public function suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendExpiredSubscriptionPolicy() , neu DateTimeImmutable());self::assertTrue($result); }/** * @test */public function suspending_an_subscription_before_a_one_month_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable('2020-01-01'));$result = $sut->suspend(new CanSuspendAfterOneMonthPolicy(), neu DateTimeImmutable('2020-01-10'));self::assertFalse($result); }/** * @test */public function suspending_an_subscription_after_a_one_month_is_possible(): void{$sut = new Subscription(new DateTimeImmutable('2020-01-01'));$result = $sut->suspend(new CanSuspendAfterOneMonthPolicy(), neu DateTimeImmutable('2020-02-02'));self::assertTrue($result); } }
Wie testet man eine Klasse wie diese richtig?
Klasse ApplicationService {public function __construct(private readonly OrderRepository $orderRepository,private readonly FormRepository $formRepository) {}public function changeFormStatus(int $orderId): void{$order = $this->orderRepository->getById($orderId);$soapResponse = $ this->getSoapClient()->getStatusByOrderId($orderId);$form = $this->formRepository->getByOrderId($orderId);$form->setStatus($soapResponse['status']);$form->setModifiedAt(new DateTimeImmutable());if ($soapResponse['status'] = == 'akzeptiert') {$order->setStatus('paid'); }$this->formRepository->save($form);$this->orderRepository->save($order); }private function getSoapClient(): SoapClient{return new SoapClient('https://legacy_system.pl/Soap/WebService', []); } }
[!TIP|style:flat|label:GOOD]
Es ist erforderlich, einen überkomplizierten Code in separate Klassen aufzuteilen.
Abschlussklasse ApplicationService {public function __construct(private readonly OrderRepositoryInterface $orderRepository,private readonly FormRepositoryInterface $formRepository,private readonly FormApiInterface $formApi,private readonly ChangeFormStatusService $changeFormStatusService) {}public function changeFormStatus(int $orderId): void{$order = $this->orderRepository ->getById($orderId);$form = $this->formRepository->getByOrderId($orderId);$status = $this->formApi->getStatusByOrderId($orderId);$this->changeFormStatusService->changeStatus($order, $form, $status);$this ->formRepository->save($form);$this->orderRepository->save($order); } }
letzte Klasse ChangeFormStatusService {public function changeStatus(Order $order, Form $form, string $formStatus): void{$status = FormStatus::createFromString($formStatus);$form->changeStatus($status);if ($form->isAccepted( )) {$order->changeStatus(OrderStatus::paid()); } } }
Die letzte Klasse ChangingFormStatusTest erweitert TestCase {/** * @test */public function Changing_a_form_status_to_accepted_changes_an_order_status_to_paid(): void{$order = new Order();$form = new Form();$status = 'accepted';$sut = new ChangeFormStatusService();$sut ->changeStatus($order, $form, $status);self::assertTrue($form->isAccepted());self::assertTrue($order->isPaid()); }/** * @test */public function Changing_a_form_status_to_refused_not_changes_an_order_status(): void{$order = new Order();$form = new Form();$status = 'new';$sut = new ChangeFormStatusService();$sut ->changeStatus($order, $form, $status);self::assertFalse($form->isAccepted());self::assertFalse($order->isPaid()); } }
Allerdings sollte ApplicationService wahrscheinlich durch einen Integrationstest nur mit simuliertem FormApiInterface getestet werden.
[!WARNING|style:flat|label:BAD]
Abschlussklasse Kunde {public function __construct(private string $name) {}public function getName(): string{return $this->name; }public function setName(string $name): void{$this->name = $name; } }
Die letzte Klasse CustomerTest erweitert TestCase {public function testSetName(): void{$customer = new Customer('Jack');$customer->setName('John');self::assertSame('John', $customer->getName()); } }
letzte Klasse EventSubscriber {public static function getSubscribedEvents(): array{return ['event' => 'onEvent']; }öffentliche Funktion onEvent(): void{ } }
Die letzte Klasse EventSubscriberTest erweitert TestCase {public function testGetSubscribedEvents(): void{$result = EventSubscriber::getSubscribedEvents();self::assertSame(['event' => 'onEvent'], $result); } }
[!ATTENTION] Den Code ohne komplizierte Logik zu testen ist sinnlos, führt aber auch zu fragilen Tests.
[!WARNING|style:flat|label:BAD]
letzte Klasse UserRepository {public function __construct(private readonly Connection $connection) {}public function getUserNameByEmail(string $email): ?array{return $this->connection->createQueryBuilder() ->von('Benutzer', 'u') ->where('u.email = :email') ->setParameter('email', $email) ->execute() ->fetch(); } }
Die letzte Klasse TestUserRepository erweitert TestCase {public function testGetUserNameByEmail(): void{$email = '[email protected]';$connection = $this->createMock(Connection::class);$queryBuilder = $this->createMock(QueryBuilder::class); $result = $this->createMock(ResultStatement::class);$userRepository = new UserRepository($connection);$connection->expects($this->once()) ->method('createQueryBuilder') ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->Methode('von') ->with('user', 'u') ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->Methode('wo') ->with('u.email = :email') ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) ->method('setParameter') ->with('email', $email) ->willReturn($queryBuilder);$queryBuilder->expects($this->once()) -> Methode ('Execute') -> WillReturn ($ result); $ result-> erwartet ($ this-> einmal ()) -> Methode ('Fetch') -> WillReturn (['E-Mail' => $ mail]); $ result = $ userRepository-> getUnernamebyemail ($ mail); self :: assertSame (['E-Mail' => $ E-Mail, $ result); } }
[!ATTENTION] Das Testen von Repositorys auf diese Weise führt zu fragilen Tests und das Refactoring ist schwierig. Um Repositorys zu testen, schreiben Sie Integrationstests.
[!TIP|style:flat|label:GOOD]
Finale Klasse Goodest erweitert TestCase {Private AbonnementFactory $ Sut; public function setup (): void {$ this-> sut = new ABCONDSCRIPTIONFACTORY (); }/** * @test */public function erstellt_a_subscription_for_a_given_date_range (): void {$ result = $ this-> sut-> create (new DateTimeMimmable (), New DateTimeMimmicle ('jetzt +1 Jahr'); Selbst :: (Abonnement :: Klasse, $ Ergebnis); }/** * @test */public function throws_an_exception_on_invalid_date_range (): void {$ this-> erwartException (createSubScriptionException :: class); $ result = $ this-> sut-> create (neuer DateTimeMimmable ('jetzt -1 Jahr'), neuer DateTimeMimmuth ()); } }
Notiz
Der beste Fall für die Verwendung der setUp-Methode ist das Testen zustandsloser Objekte.
Jede in setUp
vorgenommene Konfiguration koppelt Tests miteinander und hat Auswirkungen auf alle Tests.
Es ist besser, einen gemeinsamen Status zwischen Tests zu vermeiden und den Anfangsstatus entsprechend der Testmethode zu konfigurieren.
Die Lesbarkeit ist im Vergleich zur Konfiguration, die mit der richtigen Testmethode vorgenommen wurde, schlechter.
[!TIP|style:flat|label:BETTER]
Final Class BetterTest erweitert TestCase oder : assertRue ($ result); }/** * @test */public function suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible (): void {$ sut = $ this-> createanActiveSubscription (); : assertRue ($ result); }/** * @test */public function suspending_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible (): void {$ sut = $ this-> createeNewsubscription (); : assertfalse ($ result); } private function createOnewsubScription (): Abonnement {Neues Abonnement zurückgeben (neuer DateTimeMimmable ()); } private function CreateanActiveSubScription (): Abonnement {$ subscription = neues Abonnement (neuer DateTimeMimmable ()); $ Abonnement-> active (); Rückgabe $ Abonnement; } }
Notiz
Dieser Ansatz verbessert die Lesbarkeit und verdeutlicht die Trennung (Code wird mehr gelesen als geschrieben).
Die Verwendung privater Helfer kann bei jeder Testmethode mühsam sein, obwohl sie explizite Absichten angeben.
Um ähnliche Testobjekte zwischen mehreren Testklassen zu teilen, verwenden Sie:
Objektmutter
Baumeister
[!WARNING|style:flat|label:BAD]
Kunde der endgültige Klasse {private customerType $ type; {$ this-> type = customerType :: Normal (); $ this-> DiscountCalculationPolicy = new NormalDiscountPolicy (); } public function makevip (): void {$ this-> type = customerType :: VIP (); } public function getCustomerType (): customerType {return $ this-> Typ; } public function getSercentagedScount (): int {return $ this-> DiscountCalculationPolicy-> GetpercentagedScount (); } }
endgültige Klasse InvalidTest erweitert TestCase {public function testMakevip (): void {$ sut = new customer (); } }
[!TIP|style:flat|label:GOOD]
Kunde der endgültige Klasse {private customerType $ type; {$ this-> type = customerType :: Normal (); $ this-> DiscountCalculationPolicy = new NormalDiscountPolicy (); } public function makevip (): void {$ this-> type = customerType :: VIP (); } public function getSercentagedScount (): int {return $ this-> DiscountCalculationPolicy-> GetpercentagedScount (); } }
endgültige Klasse ValidTest erweitert TestCase {/** * @test */public function a_vip_customer_has_a_25_percentage_discount (): void {$ sut = new customer (); } }
[!ATTENTION] Das Hinzufügen von zusätzlichem Produktionscode (z. B. Getter getCustomerType()) nur zur Überprüfung des Status in Tests ist eine schlechte Praxis. Es sollte durch einen anderen domänensignifikanten Wert überprüft werden (in diesem Fall getPercentageDiscount()). Natürlich kann es manchmal schwierig sein, einen anderen Weg zur Überprüfung des Vorgangs zu finden, und wir können gezwungen sein, zusätzlichen Produktionscode hinzuzufügen, um die Korrektheit in Tests zu überprüfen, aber wir sollten versuchen, dies zu vermeiden.
Final Class DiscountCalculator {öffentliche Funktion berechnen (int $ isvipfromyears): int { Assert :: Greaterthaneq ($ isvipfromyears, 0); Rückgabe min (($ isvipfromyears * 10) + 3, 80); } }
[!WARNING|style:flat|label:BAD]
endgültige Klasse InvalidTest erweitert TestCase oder ); } public function RabattDataprovider (): Array {return [ [0, 0 * 10 + 3], // Verlaufdomänendetails [1, 1 * 10 + 3],, [5, 5 * 10 + 3], [8, 80] ]; } }
[!TIP|style:flat|label:GOOD]
endgültige Klasse ValidTest erweitert TestCase oder ); } public function RabattDataprovider (): Array {return [ [0, 3], [1, 13], [5, 53], [8, 80] ]; } }
Notiz
Duplizieren Sie die Produktionslogik nicht in Tests. Überprüfen Sie die Ergebnisse einfach anhand fest codierter Werte.
[!WARNING|style:flat|label:BAD]
Klassenrabattcalculator {public function CalculateInternaldiscount (int $ isvipFromyears): int { Assert :: Greaterthaneq ($ isvipfromyears, 0); Rückgabe min (($ isvipfromyears * 10) + 3, 80); } public function calculateadditionaldiscountFromexternalalStem (): int {// Daten von einem externen System zur Berechnung eines Discountreturn 5; } }
Klassenbestand {public function __construct (private readonly diskontcalculator $ contabatCalculator) {} öffentliche Funktion GetTotalpriceWithDiscount (int $ TotalPrice, int $ vipfromdays): int {$ Internaldiscount = $ this- $ this-> RabattCalculator-> calculateadditionaldiscountFromexternalalsystem (); } }
endgültige Klasse InvalidTest erweitert TestCase oder ['CalculateadditionAldiscountFromexternalSystem']); , $ vipdaysfrom)); } public function orderDataprovider (): array {return [return [ [1000, 0, 920], [500, 1, 410], [644, 5, 270], ]; } }
[!TIP|style:flat|label:GOOD]
Schnittstelle externAdcountCalculatorInterface {public function calculate (): int; }
Finale Klasse InternaldiscountCalculator {öffentliche Funktion berechnen (int $ isvipfromyears): int { Assert :: Greaterthaneq ($ isvipfromyears, 0); Rückgabe min (($ isvipfromyears * 10) + 3, 80); } }
endgültige Klassenorderservice {public function __construct (private readonly InternaldiscountCalculator $ RabattCalculator, private Readonly externAdcountCalculatorinterface $ externScountCalculator) {} öffentliche Funktion GetTotalpriceWithddddddDiscount (Int $ TotalPrice, Int $ vipfromdays): Int {$ $ $ $ this- $ externaldiscount = $ this-> externAldiscountCalculator-> calculate (); $ rabattesum = $ Internaldiscount + $ externAdcount; return $ TotalPrice-(int) ceil (($ TotalPrice * $ rabattesum) / 100); } }
endgültige Klasse ValidTest erweitert TestCase oder } }; $ sut = neuer ordnerService (neuer internaldiscountCalculator (), $ externAdcountCalculator); self :: assertSame ($ erwartet, $ sut-> gettotalpriceWithDiscount ($ TotalPrice, $ vipdaysfrom); } public function orderDataprovider (): array {return [return [ [1000, 0, 920], [500, 1, 410], [644, 5, 270], ]; } }
Notiz
Die Notwendigkeit, eine konkrete Klasse zu verspotten, um einen Teil ihres Verhaltens zu ersetzen, bedeutet, dass diese Klasse wahrscheinlich zu kompliziert ist und gegen das Prinzip der Einzelverantwortung verstößt.
endgültige Klasse OrderItem {public function __construct (public readonly int $ insgesamt) {} }
endgültige Klassenbestellung {/** * @param orderItem [] $ items * @param int $ transportcost */public function __construct (privates Array $ items, privat int $ transportcost) {} öffentliche Funktion Gettotal (): int {return $ this-> getItemstotal () + $ this-> transportcost; } private Funktion getItemstotal (): int {return array_reduce (array_map (fn (orderItem $ item) => $ item-> Gesamt, $ this-> items), fn (int $ sum, int $ insgesamt) => $ sum + = $ Total, 0); } }
[!WARNING|style:flat|label:BAD]
endgültige Klasse InvalidTest erweitert TestCase oder }/** * @test * @dataprovider orderItemsDataprovider */public function get_items_total_returns_a_total_cost_of_all_items (order order order order, int $ erwarteteTotal): } öffentliche FunktionsordnungenDataprovider (): Array {return [ [New Order ([New OrderItem (20), Neue OrderItem (20), Neue OrderItem (20)], 15), 75], [Neue Order ([Neue OrderItem (20), Neue OrderItem (30), Neue OrderItem (40)], 0), 90], [New Order ([New OrderItem (99), Neue OrderItem (99), Neue OrderItem (99)], 9), 306] ]; } public function orderItemsDataprovider (): Array {return [ [Neue Order ([Neue OrderItem (20), Neue OrderItem (20), Neue OrderItem (20)], 15), 60], [Neue Order ([Neue OrderItem (20), Neue OrderItem (30), Neue OrderItem (40)], 0), 90], [New Order ([New OrderItem (99), New OrderItem (99), Neue OrderItem (99)], 9), 297] ]; } private Funktion invokePrivatemethodgetItemstotal (Order & $ Order): int {$ reflection = new ReflectionClass (get_class ($ order)); $ method = $ reflection-> getMethod ('getItemstotal'); $ method-> invokeargs ($ order, []); } }
[!TIP|style:flat|label:GOOD]
endgültige Klasse ValidTest erweitert TestCase oder } öffentliche FunktionsordnungenDataprovider (): Array {return [ [New Order ([New OrderItem (20), Neue OrderItem (20), Neue OrderItem (20)], 15), 75], [Neue Order ([Neue OrderItem (20), Neue OrderItem (30), Neue OrderItem (40)], 0), 90], [New Order ([New OrderItem (99), Neue OrderItem (99), Neue OrderItem (99)], 9), 306] ]; } }
[!ATTENTION] Tests sollten nur die öffentliche API überprüfen.
Die Zeit ist eine volatile Abhängigkeit, da sie nicht deterministisch ist. Jeder Aufruf gibt ein anderes Ergebnis zurück.
[!WARNING|style:flat|label:BAD]
letzte Klassenuhr {public static datetime | null $ currentDateTime = null; öffentliche statische Funktion getCurrentDateTime (): DateTime {if (null === self :: $ currentDatetime) {self :: $ currentDateTime = new DateTime (); } return self :: $ currentDatetime; } öffentliche statische Funktionssatz (DateTime $ DateTime): void {self :: $ currentDatetime = $ DateTime; } public static function reset (): void {self :: $ currentDatetime = null; } }
Kunde der endgültige Klasse {private datetime $ createdat; öffentliche Funktion __construct () {$ this-> createdat = clock :: getCurrentDatetime (); } public function isvip (): bool {return $ this-> create-> diff (clock :: getCurrentDatetime ())-> y> = 1; } }
endgültige Klasse InvalidTest erweitert TestCase {/** * @test */public function a_customer_registered_more_than_a_one_year_ago_is_a_vip (): void { Uhr :: set (neuer DateTime ('2019-01-01')); $ Sut = New Customer (); Uhr :: reset (); // Sie müssen sich daran erinnern, den gemeinsam genutzten Staaten zurückzusetzen :: AssertRue ($ sut-> isvip ()); }/** * @test */public function a_customer_registered_less_than_a_one_year_ago_is_not_a_vip (): void { Uhr :: set ((neuer DateTime ())-> sub (neuer Datumsinterval ('p2m'))); $ sut = new Customer (); Uhr :: reset (); // Sie müssen sich daran erinnern, den gemeinsam genutzten Staaten zurückzusetzen :: AssertFalse ($ sut-> isvip ()); } }
[!TIP|style:flat|label:GOOD]
Schnittstellen -Clockinterface {public function getCurrentTime (): DateTimeMimmable; }
Die endgültige Klassenuhr implementiert Clockinterface {private Funktion __construct () { } öffentliche statische Funktion create (): self {return New self (); } public function getCurrentTime (): DateTimeMimmable {return New DateTimeMimmable (); } }
Die endgültige Klasse FixedClock implementiert Clockinterface {private Funktion __construct (private readonly datetimimmable $ fixedDate) {} öffentliche statische Funktion create (DateTimeMimmable $ fixedDate): self {Neues Selbst zurückgeben ($ fixedDate); } public function getCurrentTime (): DateTimeMimmable {return $ this-> fixedDate; } }
Kunde der endgültige Klasse {public function __construct (private readonly datetimimmable $ createdat) {} public function isvip (DateTimeMimmable $ currentDate): bool {return $ this-> erstellt-> diff ($ currentDate)-> y> = 1; } }
endgültige Klasse ValidTest erweitert TestCase oder AssertRue ($ Sut-> isvip (FixedClock :: Create (neu DateTimeMimmable ('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 (neu DateTimeMimmable ('2019-01-01'))-> getCurrenttime ()); self :: assertfalse ($ sut-> isvip (FixedClock :: create (New DateTimeMimmable ('2019-05-02')-> GetCurentTime ('2019-05-02')-> GetCurentTime (('2019-05-02')-> GetCurrentTime (('2019-05-02')-> getCurrentTime (('2019-05-02')- ))); } }
Notiz
Die Zeit- und Zufallszahlen sollten nicht direkt im Domänencode generiert werden. Um das Verhalten zu testen, müssen wir deterministische Ergebnisse haben, also müssen wir diese Werte wie im obigen Beispiel in ein Domänenobjekt einfügen.
Eine 100-prozentige Abdeckung ist nicht das Ziel oder sogar unerwünscht, da bei einer 100-prozentigen Abdeckung die Tests wahrscheinlich sehr fragil sind, was bedeutet, dass die Umgestaltung sehr schwierig sein wird. Mutationstests geben ein besseres Feedback über die Qualität der Tests. Mehr lesen
Testgetriebene Entwicklung: Mit Beispiel / Kent Beck - The Classic
Prinzipien, Praktiken und Muster von Unit-Tests / Vladimir Khorikov – das beste Buch über Tests, das ich je gelesen habe
Kamil Ruczyński
Twitter: https://twitter.com/Sarvendev
Blog: https://sarvendev.com/
LinkedIn: https://www.linkedin.com/in/kamilruczynski/