{"id":15014427,"url":"https://github.com/sarven/unit-testing-tips","last_synced_at":"2025-05-14T20:10:38.920Z","repository":{"id":45725878,"uuid":"337831958","full_name":"sarven/unit-testing-tips","owner":"sarven","description":"Unit testing tips by examples in PHP","archived":false,"fork":false,"pushed_at":"2025-01-03T16:36:31.000Z","size":135,"stargazers_count":1167,"open_issues_count":1,"forks_count":62,"subscribers_count":38,"default_branch":"main","last_synced_at":"2025-04-14T04:01:01.225Z","etag":null,"topics":["best-practices","php","phpunit","phpunit-tests","tdd","testing","tests","unit-testing","unit-tests"],"latest_commit_sha":null,"homepage":"https://testing-tips.sarvendev.com/","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sarven.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-10T19:31:41.000Z","updated_at":"2025-04-14T03:12:22.000Z","dependencies_parsed_at":"2025-01-26T14:00:26.524Z","dependency_job_id":"788471f3-1d7e-4771-8d05-957cbf03751b","html_url":"https://github.com/sarven/unit-testing-tips","commit_stats":{"total_commits":86,"total_committers":6,"mean_commits":"14.333333333333334","dds":0.08139534883720934,"last_synced_commit":"48ada486ae750f3d08a4408ffcbb27b5b0ccd69b"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sarven%2Funit-testing-tips","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sarven%2Funit-testing-tips/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sarven%2Funit-testing-tips/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sarven%2Funit-testing-tips/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sarven","download_url":"https://codeload.github.com/sarven/unit-testing-tips/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254219374,"owners_count":22034397,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["best-practices","php","phpunit","phpunit-tests","tdd","testing","tests","unit-testing","unit-tests"],"created_at":"2024-09-24T19:45:36.966Z","updated_at":"2025-05-14T20:10:38.869Z","avatar_url":"https://github.com/sarven.png","language":"HTML","readme":"[![GitHub stars](https://img.shields.io/github/stars/sarven/unit-testing-tips.svg?style=social\u0026label=Star)](https://github.com/sarven/unit-testing-tips/)\n[![GitHub watchers](https://img.shields.io/github/watchers/sarven/unit-testing-tips.svg?style=social\u0026label=Watch)](https://github.com/sarven/unit-testing-tips/watchers/)\n[![GitHub forks](https://img.shields.io/github/forks/sarven/unit-testing-tips.svg?style=social\u0026label=Fork)](https://github.com/sarven/unit-testing-tips/forks/)\n[![GitHub contributors](https://img.shields.io/github/contributors/sarven/unit-testing-tips.svg)](https://github.com/sarven/unit-testing-tips/graphs/contributors/)\n\n# Testing tips\n\nIn these times, the benefits of writing unit tests are huge.\nI think that most of the recently started projects contain any unit tests.\nIn enterprise applications with a lot of business logic, unit tests are the most important tests,\nbecause they are fast and can us instantly assure that our implementation is correct.\nHowever, I often see a problem with good tests in projects, though these tests' benefits are only huge when you have good unit tests.\nSo in these examples, I will try to share some tips on what to do to write good unit tests.\n\n**Easy-to-read version:** [https://testing-tips.sarvendev.com/](https://testing-tips.sarvendev.com/)\n\n## Author\n\n:construction_worker: **Kamil Ruczyński** \n    \n[![Twitter](https://img.shields.io/twitter/follow/Sarvendev?label=Follow\u0026style=social)](https://twitter.com/Sarvendev)\n[![Github](https://img.shields.io/github/followers/sarven?label=Follow\u0026style=social)](https://github.com/sarven)\n\n**Blog:** [https://sarvendev.com/](https://sarvendev.com/)   \n**LinkedIn:** [https://www.linkedin.com/in/kamilruczynski/](https://www.linkedin.com/in/kamilruczynski/)\n\n### Support\nYour support means the world to me!\nIf you've enjoyed this guide and find value in the knowledge shared,\nconsider supporting me on BuyMeCoffee:\n\n[![BuyMeCoffee](./assets/bmc-button.png ':size=150')](https://www.buymeacoffee.com/sarvendev)\n\nor simply leaving a star on the repository and\nfollowing me on Twitter and Github to be up-to-date with all updates.\nYour generosity fuels my passion for creating more insightful content for you.\n\nIf you have any improvement ideas or a topic to write about, feel free to prepare a pull request or just let me know.\n\n## Free ebook – Unit testing tips\n\n[![FreeEbookUnitTestingTips](./assets/ebook-unit-testing-tips.png ':size=300')](https://sarvendev.com/free-ebook-unit-testing-tips/)\n\nSubscribe and master unit testing with my FREE eBook! 🚀    \n:point_right: [Details](https://sarvendev.com/free-ebook-unit-testing-tips/)\n\nI still have a pretty long TODO list of improvements to this guide about Unit Testing and I will introduce them in the near future.\n\n## Table of Contents\n\n1. [Introduction](#introduction)\n2. [Author](#author)\n3. [Test doubles](#test-doubles)\n4. [Naming](#naming)\n5. [AAA pattern](#aaa-pattern)\n6. [Object mother](#object-mother)\n7. [Builder](#builder)\n8. [Assert object](#assert-object)\n9. [Parameterized test](#parameterized-test)\n10. [Two schools of unit testing](#two-schools-of-unit-testing)\n    * [Classical](#classical)\n    * [Mockist](#mockist)\n    * [Dependencies](#dependencies)\n11. [Mock vs Stub](#mock-vs-stub)\n12. [Three styles of unit testing](#three-styles-of-unit-testing)\n    * [Output](#output)\n    * [State](#state)\n    * [Communication](#communication)\n13. [Functional architecture and tests](#functional-architecture-and-tests)\n14. [Observable behavior vs implementation details](#observable-behavior-vs-implementation-details)\n15. [Unit of behavior](#unit-of-behavior)\n16. [Humble pattern](#humble-pattern)\n17. [Trivial test](#trivial-test)\n18. [Fragile test](#fragile-test)\n19. [Test fixtures](#test-fixtures)\n20. [General testing anti-patterns](#general-testing-anti-patterns)\n    * [Exposing private state](#exposing-private-state)\n    * [Leaking domain details](#leaking-domain-details)\n    * [Mocking concrete classes](#mocking-concrete-classes)\n    * [Testing private methods](#testing-private-methods)\n    * [Time as a volatile dependency](#time-as-a-volatile-dependency)\n21. [100% Test Coverage shouldn't be the goal](#100-test-coverage-shouldnt-be-the-goal)\n22. [Recommended books](#recommended-books)\n\n\n## Test doubles\n\nTest doubles are fake dependencies used in tests.\n\n![Test doubles](./assets/test-doubles.jpg ':size=800')\n\n### Stubs\n\n#### Dummy\n\nA dummy is a just simple implementation that does nothing.\n\n```php\nfinal class Mailer implements MailerInterface\n{\n    public function send(Message $message): void\n    {\n    }\n}\n```\n\n#### Fake\n\nA fake is a simplified implementation to simulate the original behavior.\n\n```php\nfinal class InMemoryCustomerRepository implements CustomerRepositoryInterface\n{\n    /**\n     * @var Customer[]\n     */\n    private array $customers;\n\n    public function __construct()\n    {\n        $this-\u003ecustomers = [];\n    }\n\n    public function store(Customer $customer): void\n    {\n        $this-\u003ecustomers[(string) $customer-\u003eid()-\u003eid()] = $customer;\n    }\n\n    public function get(CustomerId $id): Customer\n    {\n        if (!isset($this-\u003ecustomers[(string) $id-\u003eid()])) {\n            throw new CustomerNotFoundException();\n        }\n\n        return $this-\u003ecustomers[(string) $id-\u003eid()];\n    }\n\n    public function findByEmail(Email $email): Customer\n    {\n        foreach ($this-\u003ecustomers as $customer) {\n            if ($customer-\u003egetEmail()-\u003eisEqual($email)) {\n                return $customer;\n            }\n        }\n\n        throw new CustomerNotFoundException();\n    }\n}\n```\n\n#### Stub\n\nA stub is the simplest implementation with a hardcoded behavior.\n\n```php\nfinal class UniqueEmailSpecificationStub implements UniqueEmailSpecificationInterface\n{\n    public function isUnique(Email $email): bool\n    {\n        return true;\n    }\n}\n```\n\n```php\n$specificationStub = $this-\u003ecreateStub(UniqueEmailSpecificationInterface::class);\n$specificationStub-\u003emethod('isUnique')-\u003ewillReturn(true);\n```\n\n### Mocks\n\n#### Spy\n\nA spy is an implementation to verify a specific behavior.\n\n```php\nfinal class Mailer implements MailerInterface\n{\n    /**\n     * @var Message[]\n     */\n    private array $messages;\n    \n    public function __construct()\n    {\n        $this-\u003emessages = [];\n    }\n\n    public function send(Message $message): void\n    {\n        $this-\u003emessages[] = $message;\n    }\n\n    public function getCountOfSentMessages(): int\n    {\n        return count($this-\u003emessages);\n    }\n}\n```\n\n#### Mock\n\nA mock is a configured imitation to verify calls on a collaborator.\n\n```php\n$message = new Message('test@test.com', 'Test', 'Test test test');\n$mailer = $this-\u003ecreateMock(MailerInterface::class);\n$mailer\n    -\u003eexpects($this-\u003eonce())\n    -\u003emethod('send')\n    -\u003ewith($this-\u003eequalTo($message));\n```\n\n\u003e [!ATTENTION] \n\u003e To verify incoming interactions, use a stub, but to verify outcoming interactions, use a mock.   \n\u003e More: [Mock vs Stub](#mock-vs-stub)\n\n### Always prefer own test double classes than those provided by a framework\n\n\u003e [!WARNING|style:flat|label:NOT GOOD]\n\n```php\nfinal class TestExample extends TestCase\n{\n    /**\n     * @test\n     */\n    public function sends_all_notifications(): void\n    {\n        $message1 = new Message();\n        $message2 = new Message();\n        $messageRepository = $this-\u003ecreateMock(MessageRepositoryInterface::class);\n        $messageRepository-\u003emethod('getAll')-\u003ewillReturn([$message1, $message2]);\n        $mailer = $this-\u003ecreateMock(MailerInterface::class);\n        $sut = new NotificationService($mailer, $messageRepository);\n\n        $mailer-\u003eexpects(self::exactly(2))-\u003emethod('send')\n            -\u003ewithConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);\n\n        $sut-\u003esend();\n    }\n}\n```\n\n\u003e [!TIP|style:flat|label:BETTER]\n\n- **Better resistance to refactoring**\n    - Using Refactor-\u003eRename on the particular method doesn't break the test\n- **Better readability**\n- **Lower cost of maintainability**\n    - Not required to learn those sophisticated mocks frameworks\n    - Just simple plain PHP code\n\n```php\nfinal class TestExample extends TestCase\n{\n    /**\n     * @test\n     */\n    public function sends_all_notifications(): void\n    {\n        $message1 = new Message();\n        $message2 = new Message();\n        $messageRepository = new InMemoryMessageRepository();\n        $messageRepository-\u003esave($message1);\n        $messageRepository-\u003esave($message2);\n        $mailer = new SpyMailer();\n        $sut = new NotificationService($mailer, $messageRepository);\n\n        $sut-\u003esend();\n        \n        $mailer-\u003eassertThatMessagesHaveBeenSent([$message1, $message2]);\n    }\n}\n```\n\n## Naming\n\n\u003e [!WARNING|style:flat|label:NOT GOOD]\n\n```php\npublic function test(): void\n{\n    $subscription = SubscriptionMother::new();\n\n    $subscription-\u003eactivate();\n\n    self::assertSame(Status::activated(), $subscription-\u003estatus());\n}\n```\n\n\u003e [!TIP|style:flat|label:Specify explicitly what you are testing]\n\n```php\npublic function sut(): void\n{\n    // sut = System under test\n    $sut = SubscriptionMother::new();\n\n    $sut-\u003eactivate();\n\n    self::assertSame(Status::activated(), $sut-\u003estatus());\n}\n``` \n\n\u003e [!WARNING|style:flat|label:NOT GOOD]\n\n```php\npublic function it_throws_invalid_credentials_exception_when_sign_in_with_invalid_credentials(): void\n{\n\n}\n\npublic function testCreatingWithATooShortPasswordIsNotPossible(): void\n{\n\n}\n\npublic function testDeactivateASubscription(): void\n{\n\n}\n```\n\n\u003e [!TIP|style:flat|label:BETTER]\n\n- **Using underscore improves readability**\n- **The name should describe the behavior, not the implementation**\n- **Use names without technical keywords. It should be readable for a non-programmer person.**\n\n```php\npublic function sign_in_with_invalid_credentials_is_not_possible(): void\n{\n\n}\n\npublic function creating_with_a_too_short_password_is_not_possible(): void\n{\n\n}\n\npublic function deactivating_an_activated_subscription_is_valid(): void\n{\n\n}\n\npublic function deactivating_an_inactive_subscription_is_invalid(): void\n{\n\n}\n```\n\n\u003e [!NOTE]\n\u003e Describing the behavior is important in testing the domain scenarios.\n\u003e If your code is just a utility one it's less important.\n\u003e \n\u003e **Why would it be useful for a non-programmer to read unit tests?**   \n\u003e \n\u003e If there is a project with complex domain logic, this logic must be very clear for everyone, so then tests describe domain details without technical keywords, and you can talk with a business in a language like in these tests.\n\u003e All code that is related to the domain should be free from technical details. A non-programmer won't be read these tests. If you want to talk about the domain these tests will be useful to know what this domain does. There will be a description without technical details e.g., returns null, throws an exception, etc. This kind of information has nothing to do with the domain, so we shouldn't use these keywords.\n\n\n## AAA pattern\n\nIt's also common Given, When, Then.\n\nSeparate three sections of the test:\n\n- **Arrange**: Bring the system under test in the desired state. Prepare dependencies, arguments and finally construct\n  the SUT.\n- **Act**: Invoke a tested element.\n- **Assert**: Verify the result, the final state, or the communication with collaborators.\n\n\u003e [!TIP|style:flat|label:GOOD]\n\n```php\npublic function aaa_pattern_example_test(): void\n{\n    //Arrange|Given\n    $sut = SubscriptionMother::new();\n\n    //Act|When\n    $sut-\u003eactivate();\n\n    //Assert|Then\n    self::assertSame(Status::activated(), $sut-\u003estatus());\n}\n```\n\n## Object mother\n\nThe pattern helps to create specific objects which can be reused in a few tests. Because of that the arrange section\nis concise and the test as a whole is more readable.\n\n```php\nfinal class SubscriptionMother\n{\n    public static function new(): Subscription\n    {\n        return new Subscription();\n    }\n\n    public static function activated(): Subscription\n    {\n        $subscription = new Subscription();\n        $subscription-\u003eactivate();\n        return $subscription;\n    }\n\n    public static function deactivated(): Subscription\n    {\n        $subscription = self::activated();\n        $subscription-\u003edeactivate();\n        return $subscription;\n    }\n}\n```\n\n```php\nfinal class ExampleTest\n{\n    public function example_test_with_activated_subscription(): void\n    {\n        $activatedSubscription = SubscriptionMother::activated();\n\n        // do something\n\n        // check something\n    }\n\n    public function example_test_with_deactivated_subscription(): void\n    {\n        $deactivatedSubscription = SubscriptionMother::deactivated();\n\n        // do something\n\n        // check something\n    }\n}\n```\n\n## Builder\n\nBuilder is another pattern that helps us to create objects in tests. Compared to Object Mother pattern Builder is better for creating\nmore complex objects.\n\n```php\nfinal class OrderBuilder\n{\n    private DateTimeImmutable|null $createdAt = null;\n\n    /**\n     * @var OrderItem[]\n     */\n    private array $items = [];\n\n    public function createdAt(DateTimeImmutable $createdAt): self\n    {\n        $this-\u003ecreatedAt = $createdAt;\n        return $this;\n    }\n\n    public function withItem(string $name, int $price): self\n    {\n        $this-\u003eitems[] = new OrderItem($name, $price);\n        return $this;\n    }\n\n    public function build(): Order\n    {\n        Assert::notEmpty($this-\u003eitems);\n\n        return new Order(\n            $this-\u003ecreatedAt ?? new DateTimeImmutable(),\n            $this-\u003eitems,\n        );\n    }\n}\n```\n\n```php\nfinal class ExampleTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function example_test_with_order_builder(): void\n    {\n        $order = (new OrderBuilder())\n            -\u003ecreatedAt(new DateTimeImmutable('2022-11-10 20:00:00'))\n            -\u003ewithItem('Item 1', 1000)\n            -\u003ewithItem('Item 2', 2000)\n            -\u003ewithItem('Item 3', 3000)\n            -\u003ebuild();\n\n        // do something\n\n        // check something\n    }\n}\n```\n\n## Assert object\n\nAssert object pattern helps write more readable assert sections. Instead of using a few asserts, we can just prepare an abstraction,\nand use natural language to describe what result is expected.\n\n```php\nfinal class ExampleTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function example_test_with_asserter(): void\n    {\n        $currentTime = new DateTimeImmutable('2022-11-10 20:00:00');\n        $sut = new OrderService();\n\n        $order = $sut-\u003ecreate($currentTime);\n\n        OrderAsserter::assertThat($order)\n            -\u003ewasCreatedAt($currentTime)\n            -\u003ehasTotal(6000);\n    }\n}\n```\n\n```php\nuse PHPUnit\\Framework\\Assert;\n\nfinal class OrderAsserter\n{\n    public function __construct(private readonly Order $order) {}\n\n    public static function assertThat(Order $order): self\n    {\n        return new OrderAsserter($order);\n    }\n\n    public function wasCreatedAt(DateTimeImmutable $createdAt): self\n    {\n        Assert::assertEquals($createdAt, $this-\u003eorder-\u003ecreatedAt);\n        return $this;\n    }\n\n    public function hasTotal(int $total): self\n    {\n        Assert::assertSame($total, $this-\u003eorder-\u003egetTotal());\n        return $this;\n    }\n}\n```\n\n## Parameterized test\n\nThe parameterized test is a good option to test the SUT with many parameters without repeating the code.\n\n\u003e [!WARNING]\n\u003e :thumbsdown: This kind of test is less readable. To increase the readability a little, negative and positive examples should be split up to different tests.\n\n```php\nfinal class ExampleTest extends TestCase\n{\n    /**\n     * @test\n     * @dataProvider getInvalidEmails\n     */\n    public function detects_an_invalid_email_address(string $email): void\n    {\n        $sut = new EmailValidator();\n\n        $result = $sut-\u003eisValid($email);\n\n        self::assertFalse($result);\n    }\n\n    /**\n     * @test\n     * @dataProvider getValidEmails\n     */\n    public function detects_an_valid_email_address(string $email): void\n    {\n        $sut = new EmailValidator();\n\n        $result = $sut-\u003eisValid($email);\n\n        self::assertTrue($result);\n    }\n\n    public function getInvalidEmails(): iterable\n    {\n        yield 'An invalid email without @' =\u003e ['test'];\n        yield 'An invalid email without the domain after @' =\u003e ['test@'];\n        yield 'An invalid email without TLD' =\u003e ['test@test'];\n        //...\n    }\n\n    public function getValidEmails(): iterable\n    {\n        yield 'A valid email with lowercase letters' =\u003e ['test@test.com'];\n        yield 'A valid email with lowercase letters and digits' =\u003e ['test123@test.com'];\n        yield 'A valid email with uppercase letters and digits' =\u003e ['Test123@test.com'];\n        //...\n    }\n}\n```\n\n\u003e [!NOTE]\n\u003e Use `yield` and add a text description to cases to improve the readability.\n\n## Two schools of unit testing\n\n### Classical (Detroit school)\n\n- The unit is a single unit of behavior, it can be a few related classes.\n\n```php\nfinal class TestExample extends TestCase\n{\n    /**\n     * @test\n     */\n    public function suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void\n    {\n        $canAlwaysSuspendPolicy = new CanAlwaysSuspendPolicy();\n        $sut = new Subscription();\n\n        $result = $sut-\u003esuspend($canAlwaysSuspendPolicy);\n\n        self::assertTrue($result);\n        self::assertSame(Status::suspend(), $sut-\u003estatus());\n    }\n}\n```\n\n### Mockist (London school)\n\n- The unit is a single class.\n- The unit should be isolated from all collaborators.\n\n```php\nfinal class TestExample extends TestCase\n{\n    /**\n     * @test\n     */\n    public function suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void\n    {\n        $canAlwaysSuspendPolicy = $this-\u003ecreateStub(SuspendingPolicyInterface::class);\n        $canAlwaysSuspendPolicy-\u003emethod('suspend')-\u003ewillReturn(true);\n        $sut = new Subscription();\n\n        $result = $sut-\u003esuspend($canAlwaysSuspendPolicy);\n\n        self::assertTrue($result);\n        self::assertSame(Status::suspend(), $sut-\u003estatus());\n    }\n}\n```\n\n\u003e [!NOTE]\n\u003e **The classical approach is better to avoid fragile tests.**\n\n### Dependencies\n\n[TODO]\n\n## Mock vs. Stub\n\nExample:\n```php\nfinal class NotificationService\n{\n    public function __construct(\n        private readonly MailerInterface $mailer,\n        private readonly MessageRepositoryInterface $messageRepository\n    ) {}\n\n    public function send(): void\n    {\n        $messages = $this-\u003emessageRepository-\u003egetAll();\n        foreach ($messages as $message) {\n            $this-\u003emailer-\u003esend($message);\n        }\n    }\n}\n```\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n- **Asserting interactions with stubs leads to fragile tests**\n\n```php\nfinal class TestExample extends TestCase\n{\n    /**\n     * @test\n     */\n    public function sends_all_notifications(): void\n    {\n        $message1 = new Message();\n        $message2 = new Message();\n        $messageRepository = $this-\u003ecreateMock(MessageRepositoryInterface::class);\n        $messageRepository-\u003emethod('getAll')-\u003ewillReturn([$message1, $message2]);\n        $mailer = $this-\u003ecreateMock(MailerInterface::class);\n        $sut = new NotificationService($mailer, $messageRepository);\n\n        $messageRepository-\u003eexpects(self::once())-\u003emethod('getAll');\n        $mailer-\u003eexpects(self::exactly(2))-\u003emethod('send')\n            -\u003ewithConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);\n\n        $sut-\u003esend();\n    }\n}\n```\n\n\u003e [!TIP|style:flat|label:GOOD]\n\n```php\nfinal class TestExample extends TestCase\n{\n    /**\n     * @test\n     */\n    public function sends_all_notifications(): void\n    {\n        $message1 = new Message();\n        $message2 = new Message();\n        $messageRepository = new InMemoryMessageRepository();\n        $messageRepository-\u003esave($message1);\n        $messageRepository-\u003esave($message2);\n        $mailer = $this-\u003ecreateMock(MailerInterface::class);\n        $sut = new NotificationService($mailer, $messageRepository);\n\n        // Removed asserting interactions with the stub\n        $mailer-\u003eexpects(self::exactly(2))-\u003emethod('send')\n            -\u003ewithConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);\n\n        $sut-\u003esend();\n    }\n}\n```\n\n\u003e [!TIP|style:flat|label:EVEN BETTER USING SPY]\n\n\n```php\nfinal class TestExample extends TestCase\n{\n    /**\n     * @test\n     */\n    public function sends_all_notifications(): void\n    {\n        $message1 = new Message();\n        $message2 = new Message();\n        $messageRepository = new InMemoryMessageRepository();\n        $messageRepository-\u003esave($message1);\n        $messageRepository-\u003esave($message2);\n        $mailer = new SpyMailer();\n        $sut = new NotificationService($mailer, $messageRepository);\n\n        $sut-\u003esend();\n        \n        $mailer-\u003eassertThatMessagesHaveBeenSent([$message1, $message2]);\n    }\n}\n```\n\n## Three styles of unit testing\n\n### Output\n\n\u003e [!TIP|style:flat|label:The best option]\n\n\n- **The best resistance to refactoring**\n- **The best accuracy**\n- **The lowest cost of maintainability**\n- **If it is possible, you should prefer this kind of test**\n\n```php\nfinal class ExampleTest extends TestCase\n{\n    /**\n     * @test\n     * @dataProvider getInvalidEmails\n     */\n    public function detects_an_invalid_email_address(string $email): void\n    {\n        $sut = new EmailValidator();\n\n        $result = $sut-\u003eisValid($email);\n\n        self::assertFalse($result);\n    }\n\n    /**\n     * @test\n     * @dataProvider getValidEmails\n     */\n    public function detects_an_valid_email_address(string $email): void\n    {\n        $sut = new EmailValidator();\n\n        $result = $sut-\u003eisValid($email);\n\n        self::assertTrue($result);\n    }\n\n    public function getInvalidEmails(): array\n    {\n        return [\n            ['test'],\n            ['test@'],\n            ['test@test'],\n            //...\n        ];\n    }\n\n    public function getValidEmails(): array\n    {\n        return [\n            ['test@test.com'],\n            ['test123@test.com'],\n            ['Test123@test.com'],\n            //...\n        ];\n    }\n}\n```\n\n### State\n\n\u003e [!WARNING|style:flat|label:Worse option]\n\n- **Worse resistance to refactoring**\n- **Worse accuracy**\n- **Higher cost of maintainability**\n\n```php\nfinal class ExampleTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function adding_an_item_to_cart(): void\n    {\n        $item = new CartItem('Product');\n        $sut = new Cart();\n\n        $sut-\u003eaddItem($item);\n\n        self::assertSame(1, $sut-\u003egetCount());\n        self::assertSame($item, $sut-\u003egetItems()[0]);\n    }\n}\n```\n\n### Communication\n\n\u003e [!ATTENTION|style:flat|label:The worst option]\n\n- **The worst resistance to refactoring**\n- **The worst accuracy**\n- **The highest cost of maintainability**\n\n```php\nfinal class ExampleTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function sends_all_notifications(): void\n    {\n        $message1 = new Message();\n        $message2 = new Message();\n        $messageRepository = new InMemoryMessageRepository();\n        $messageRepository-\u003esave($message1);\n        $messageRepository-\u003esave($message2);\n        $mailer = $this-\u003ecreateMock(MailerInterface::class);\n        $sut = new NotificationService($mailer, $messageRepository);\n\n        $mailer-\u003eexpects(self::exactly(2))-\u003emethod('send')\n            -\u003ewithConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);\n\n        $sut-\u003esend();\n    }\n}\n```\n\n## Functional architecture and tests\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n```php\nfinal class NameService\n{\n    public function __construct(private readonly CacheStorageInterface $cacheStorage) {}\n\n    public function loadAll(): void\n    {\n        $namesCsv = array_map('str_getcsv', file(__DIR__.'/../names.csv'));\n        $names = [];\n\n        foreach ($namesCsv as $nameData) {\n            if (!isset($nameData[0], $nameData[1])) {\n                continue;\n            }\n\n            $names[] = new Name($nameData[0], new Gender($nameData[1]));\n        }\n\n        $this-\u003ecacheStorage-\u003estore('names', $names);\n    }\n}\n```\n\n**How to test a code like this? It is possible only with an integration test because it directly uses\nan infrastructure code related to a file system.**\n\n\u003e [!TIP|style:flat|label:GOOD]\n\nLike in functional architecture, we need to separate a code with side effects and code that contains only logic.\n\n```php\nfinal class NameParser\n{\n    /**\n     * @param array\u003cstring[]\u003e $namesData\n     * @return Name[]\n     */\n    public function parse(array $namesData): array\n    {\n        $names = [];\n\n        foreach ($namesData as $nameData) {\n            if (!isset($nameData[0], $nameData[1])) {\n                continue;\n            }\n\n            $names[] = new Name($nameData[0], new Gender($nameData[1]));\n        }\n\n        return $names;\n    }\n}\n```\n\n```php\nfinal class CsvNamesFileLoader\n{\n    public function load(): array\n    {\n        return array_map('str_getcsv', file(__DIR__.'/../names.csv'));\n    }\n}\n```\n\n```php\nfinal class ApplicationService\n{\n    public function __construct(\n        private readonly CsvNamesFileLoader $fileLoader,\n        private readonly NameParser $parser,\n        private readonly CacheStorageInterface $cacheStorage\n    ) {}\n\n    public function loadNames(): void\n    {\n        $namesData = $this-\u003efileLoader-\u003eload();\n        $names = $this-\u003eparser-\u003eparse($namesData);\n        $this-\u003ecacheStorage-\u003estore('names', $names);\n    }\n}\n```\n\n```php\nfinal class ValidUnitExampleTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function parse_all_names(): void\n    {\n        $namesData = [\n            ['John', 'M'],\n            ['Lennon', 'U'],\n            ['Sarah', 'W']\n        ];\n        $sut = new NameParser();\n\n        $result = $sut-\u003eparse($namesData);\n        \n        self::assertSame(\n            [\n                new Name('John', new Gender('M')),\n                new Name('Lennon', new Gender('U')),\n                new Name('Sarah', new Gender('W'))\n            ],\n            $result\n        );\n    }\n}\n```\n\n## Observable behavior vs. implementation details\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n```php\nfinal class ApplicationService\n{\n    public function __construct(private readonly SubscriptionRepositoryInterface $subscriptionRepository) {}\n\n    public function renewSubscription(int $subscriptionId): bool\n    {\n        $subscription = $this-\u003esubscriptionRepository-\u003efindById($subscriptionId);\n\n        if (!$subscription-\u003egetStatus()-\u003eisEqual(Status::expired())) {\n            return false;\n        }\n\n        $subscription-\u003esetStatus(Status::active());\n        $subscription-\u003esetModifiedAt(new \\DateTimeImmutable());\n        return true;\n    }\n}\n```\n\n```php\nfinal class Subscription\n{\n    public function __construct(private Status $status, private \\DateTimeImmutable $modifiedAt) {}\n\n    public function getStatus(): Status\n    {\n        return $this-\u003estatus;\n    }\n\n    public function setStatus(Status $status): void\n    {\n        $this-\u003estatus = $status;\n    }\n\n    public function getModifiedAt(): \\DateTimeImmutable\n    {\n        return $this-\u003emodifiedAt;\n    }\n\n    public function setModifiedAt(\\DateTimeImmutable $modifiedAt): void\n    {\n        $this-\u003emodifiedAt = $modifiedAt;\n    }\n}\n```\n\n```php\nfinal class InvalidTestExample extends TestCase\n{\n    /**\n     * @test\n     */\n    public function renew_an_expired_subscription_is_possible(): void\n    {\n        $modifiedAt = new \\DateTimeImmutable();\n        $expiredSubscription = new Subscription(Status::expired(), $modifiedAt);\n        $sut = new ApplicationService($this-\u003ecreateRepository($expiredSubscription));\n\n        $result = $sut-\u003erenewSubscription(1);\n\n        self::assertSame(Status::active(), $expiredSubscription-\u003egetStatus());\n        self::assertGreaterThan($modifiedAt, $expiredSubscription-\u003egetModifiedAt());\n        self::assertTrue($result);\n    }\n\n    /**\n     * @test\n     */\n    public function renew_an_active_subscription_is_not_possible(): void\n    {\n        $modifiedAt = new \\DateTimeImmutable();\n        $activeSubscription = new Subscription(Status::active(), $modifiedAt);\n        $sut = new ApplicationService($this-\u003ecreateRepository($activeSubscription));\n\n        $result = $sut-\u003erenewSubscription(1);\n\n        self::assertSame($modifiedAt, $activeSubscription-\u003egetModifiedAt());\n        self::assertFalse($result);\n    }\n    \n    private function createRepository(Subscription $subscription): SubscriptionRepositoryInterface\n    {\n        return new class ($expiredSubscription) implements SubscriptionRepositoryInterface {\n            public function __construct(private readonly Subscription $subscription) {} \n            \n            public function findById(int $id): Subscription\n            {\n                return $this-\u003esubscription;\n            }\n        };\n    }\n}\n```\n\n\u003e [!TIP|style:flat|label:GOOD]\n\n```php\nfinal class ApplicationService\n{\n    public function __construct(\n        private readonly SubscriptionRepositoryInterface $subscriptionRepository\n    ) {}\n\n    public function renewSubscription(int $subscriptionId): bool\n    {\n        $subscription = $this-\u003esubscriptionRepository-\u003efindById($subscriptionId);\n        return $subscription-\u003erenew(new \\DateTimeImmutable());\n    }\n}\n```\n\n```php\nfinal class Subscription\n{\n    private Status $status;\n    private \\DateTimeImmutable $modifiedAt;\n\n    public function __construct(\\DateTimeImmutable $modifiedAt)\n    {\n        $this-\u003estatus = Status::new();\n        $this-\u003emodifiedAt = $modifiedAt;\n    }\n\n    public function renew(\\DateTimeImmutable $modifiedAt): bool\n    {\n        if (!$this-\u003estatus-\u003eisEqual(Status::expired())) {\n            return false;\n        }\n\n        $this-\u003estatus = Status::active();\n        $this-\u003emodifiedAt = $modifiedAt;\n        return true;\n    }\n\n    public function active(\\DateTimeImmutable $modifiedAt): void\n    {\n        //simplified\n        $this-\u003estatus = Status::active();\n        $this-\u003emodifiedAt = $modifiedAt;\n    }\n\n    public function expire(\\DateTimeImmutable $modifiedAt): void\n    {\n        //simplified\n        $this-\u003estatus = Status::expired();\n        $this-\u003emodifiedAt = $modifiedAt;\n    }\n\n    public function isActive(): bool\n    {\n        return $this-\u003estatus-\u003eisEqual(Status::active());\n    }\n}\n```\n\n```php\nfinal class ValidTestExample extends TestCase\n{\n    /**\n     * @test\n     */\n    public function renew_an_expired_subscription_is_possible(): void\n    {\n        $expiredSubscription = SubscriptionMother::expired();\n        $sut = new ApplicationService($this-\u003ecreateRepository($expiredSubscription));\n\n        $result = $sut-\u003erenewSubscription(1);\n\n        // skip checking modifiedAt as it's not a part of observable behavior. To check this value we\n        // would have to add a getter for modifiedAt, probably only for test purposes.\n        self::assertTrue($expiredSubscription-\u003eisActive());\n        self::assertTrue($result);\n    }\n\n    /**\n     * @test\n     */\n    public function renew_an_active_subscription_is_not_possible(): void\n    {\n        $activeSubscription = SubscriptionMother::active();\n        $sut = new ApplicationService($this-\u003ecreateRepository($activeSubscription));\n\n        $result = $sut-\u003erenewSubscription(1);\n\n        self::assertTrue($activeSubscription-\u003eisActive());\n        self::assertFalse($result);\n    }\n    \n    private function createRepository(Subscription $subscription): SubscriptionRepositoryInterface\n    {\n        return new class ($expiredSubscription) implements SubscriptionRepositoryInterface {\n            public function __construct(private readonly Subscription $subscription) {} \n            \n            public function findById(int $id): Subscription\n            {\n                return $this-\u003esubscription;\n            }\n        };\n    }\n}\n```\n\n\u003e [!NOTE]\n\u003e The first subscription model has a bad design. To invoke one business operation you need to call three methods. Also using getters to verify operation is not a good practice.\n\u003e In this case, it's skipped checking a change of `modifiedAt`, probably setting specific `modifiedAt` during a renew operation can be tested with an expiration business operation. The getter for `modifiedAt` is not required.\n\u003e Of course, there are cases where finding the possibility to avoid getters provided only for tests will be very hard, but always we should try not to introduce them.\n\n## Unit of behavior\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n```php\nclass CannotSuspendExpiredSubscriptionPolicy implements SuspendingPolicyInterface\n{\n    public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool\n    {\n        if ($subscription-\u003eisExpired()) {\n            return false;\n        }\n\n        return true;\n    }\n}\n```\n\n```php\nclass CannotSuspendExpiredSubscriptionPolicyTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function it_returns_false_when_a_subscription_is_expired(): void\n    {\n        $policy = new CannotSuspendExpiredSubscriptionPolicy();\n        $subscription = $this-\u003ecreateStub(Subscription::class);\n        $subscription-\u003emethod('isExpired')-\u003ewillReturn(true);\n\n        self::assertFalse($policy-\u003esuspend($subscription, new \\DateTimeImmutable()));\n    }\n\n    /**\n     * @test\n     */\n    public function it_returns_true_when_a_subscription_is_not_expired(): void\n    {\n        $policy = new CannotSuspendExpiredSubscriptionPolicy();\n        $subscription = $this-\u003ecreateStub(Subscription::class);\n        $subscription-\u003emethod('isExpired')-\u003ewillReturn(false);\n\n        self::assertTrue($policy-\u003esuspend($subscription, new \\DateTimeImmutable()));\n    }\n}\n```\n\n```php\nclass CannotSuspendNewSubscriptionPolicy implements SuspendingPolicyInterface\n{\n    public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool\n    {\n        if ($subscription-\u003eisNew()) {\n            return false;\n        }\n\n        return true;\n    }\n}\n```\n\n```php\nclass CannotSuspendNewSubscriptionPolicyTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function it_returns_false_when_a_subscription_is_new(): void\n    {\n        $policy = new CannotSuspendNewSubscriptionPolicy();\n        $subscription = $this-\u003ecreateStub(Subscription::class);\n        $subscription-\u003emethod('isNew')-\u003ewillReturn(true);\n\n        self::assertFalse($policy-\u003esuspend($subscription, new \\DateTimeImmutable()));\n    }\n\n    /**\n     * @test\n     */\n    public function it_returns_true_when_a_subscription_is_not_new(): void\n    {\n        $policy = new CannotSuspendNewSubscriptionPolicy();\n        $subscription = $this-\u003ecreateStub(Subscription::class);\n        $subscription-\u003emethod('isNew')-\u003ewillReturn(false);\n\n        self::assertTrue($policy-\u003esuspend($subscription, new \\DateTimeImmutable()));\n    }\n}\n```\n\n```php\nclass CanSuspendAfterOneMonthPolicy implements SuspendingPolicyInterface\n{\n    public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool\n    {\n        $oneMonthEarlierDate = \\DateTime::createFromImmutable($at)-\u003esub(new \\DateInterval('P1M'));\n\n        return $subscription-\u003eisOlderThan(\\DateTimeImmutable::createFromMutable($oneMonthEarlierDate));\n    }\n}\n```\n\n```php\nclass CanSuspendAfterOneMonthPolicyTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function it_returns_true_when_a_subscription_is_older_than_one_month(): void\n    {\n        $date = new \\DateTimeImmutable('2021-01-29');\n        $policy = new CanSuspendAfterOneMonthPolicy();\n        $subscription = new Subscription(new \\DateTimeImmutable('2020-12-28'));\n\n        self::assertTrue($policy-\u003esuspend($subscription, $date));\n    }\n\n    /**\n     * @test\n     */\n    public function it_returns_false_when_a_subscription_is_not_older_than_one_month(): void\n    {\n        $date = new \\DateTimeImmutable('2021-01-29');\n        $policy = new CanSuspendAfterOneMonthPolicy();\n        $subscription = new Subscription(new \\DateTimeImmutable('2020-01-01'));\n\n        self::assertTrue($policy-\u003esuspend($subscription, $date));\n    }\n}\n```\n\n```php\nclass Status\n{\n    private const EXPIRED = 'expired';\n    private const ACTIVE = 'active';\n    private const NEW = 'new';\n    private const SUSPENDED = 'suspended';\n\n    private function __construct(private readonly string $status)\n    {\n        $this-\u003estatus = $status;\n    }\n\n    public static function expired(): self\n    {\n        return new self(self::EXPIRED);\n    }\n\n    public static function active(): self\n    {\n        return new self(self::ACTIVE);\n    }\n\n    public static function new(): self\n    {\n        return new self(self::NEW);\n    }\n\n    public static function suspended(): self\n    {\n        return new self(self::SUSPENDED);\n    }\n\n    public function isEqual(self $status): bool\n    {\n        return $this-\u003estatus === $status-\u003estatus;\n    }\n}\n```\n\n```php\nclass StatusTest extends TestCase\n{\n    public function testEquals(): void\n    {\n        $status1 = Status::active();\n        $status2 = Status::active();\n\n        self::assertTrue($status1-\u003eisEqual($status2));\n    }\n\n    public function testNotEquals(): void\n    {\n        $status1 = Status::active();\n        $status2 = Status::expired();\n\n        self::assertFalse($status1-\u003eisEqual($status2));\n    }\n}\n```\n\n```php\nclass SubscriptionTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function suspending_a_subscription_is_possible_when_a_policy_returns_true(): void\n    {\n        $policy = $this-\u003ecreateMock(SuspendingPolicyInterface::class);\n        $policy-\u003eexpects($this-\u003eonce())-\u003emethod('suspend')-\u003ewillReturn(true);\n        $sut = new Subscription(new \\DateTimeImmutable());\n\n        $result = $sut-\u003esuspend($policy, new \\DateTimeImmutable());\n\n        self::assertTrue($result);\n        self::assertTrue($sut-\u003eisSuspended());\n    }\n\n    /**\n     * @test\n     */\n    public function suspending_a_subscription_is_not_possible_when_a_policy_returns_false(): void\n    {\n        $policy = $this-\u003ecreateMock(SuspendingPolicyInterface::class);\n        $policy-\u003eexpects($this-\u003eonce())-\u003emethod('suspend')-\u003ewillReturn(false);\n        $sut = new Subscription(new \\DateTimeImmutable());\n\n        $result = $sut-\u003esuspend($policy, new \\DateTimeImmutable());\n\n        self::assertFalse($result);\n        self::assertFalse($sut-\u003eisSuspended());\n    }\n\n    /**\n     * @test\n     */\n    public function it_returns_true_when_a_subscription_is_older_than_one_month(): void\n    {\n        $date = new \\DateTimeImmutable();\n        $futureDate = $date-\u003eadd(new \\DateInterval('P1M'));\n        $sut = new Subscription($date);\n\n        self::assertTrue($sut-\u003eisOlderThan($futureDate));\n    }\n\n    /**\n     * @test\n     */\n    public function it_returns_false_when_a_subscription_is_not_older_than_one_month(): void\n    {\n        $date = new \\DateTimeImmutable();\n        $futureDate = $date-\u003eadd(new \\DateInterval('P1D'));\n        $sut = new Subscription($date);\n\n        self::assertTrue($sut-\u003eisOlderThan($futureDate));\n    }\n}\n```\n\n\u003e [!ATTENTION]\n\u003e **Do not write code 1:1, 1 class : 1 test. It leads to fragile tests which make that refactoring is tough.**\n\n\u003e [!TIP|style:flat|label:GOOD]\n\n```php\nfinal class CannotSuspendExpiredSubscriptionPolicy implements SuspendingPolicyInterface\n{\n    public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool\n    {\n        if ($subscription-\u003eisExpired()) {\n            return false;\n        }\n\n        return true;\n    }\n}\n```\n\n```php\nfinal class CannotSuspendNewSubscriptionPolicy implements SuspendingPolicyInterface\n{\n    public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool\n    {\n        if ($subscription-\u003eisNew()) {\n            return false;\n        }\n\n        return true;\n    }\n}\n```\n\n```php\nfinal class CanSuspendAfterOneMonthPolicy implements SuspendingPolicyInterface\n{\n    public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool\n    {\n        $oneMonthEarlierDate = \\DateTime::createFromImmutable($at)-\u003esub(new \\DateInterval('P1M'));\n\n        return $subscription-\u003eisOlderThan(\\DateTimeImmutable::createFromMutable($oneMonthEarlierDate));\n    }\n}\n```\n\n```php\nfinal class Status\n{\n    private const EXPIRED = 'expired';\n    private const ACTIVE = 'active';\n    private const NEW = 'new';\n    private const SUSPENDED = 'suspended';\n\n    private function __construct(private readonly string $status)\n    {\n        $this-\u003estatus = $status;\n    }\n\n    public static function expired(): self\n    {\n        return new self(self::EXPIRED);\n    }\n\n    public static function active(): self\n    {\n        return new self(self::ACTIVE);\n    }\n\n    public static function new(): self\n    {\n        return new self(self::NEW);\n    }\n\n    public static function suspended(): self\n    {\n        return new self(self::SUSPENDED);\n    }\n\n    public function isEqual(self $status): bool\n    {\n        return $this-\u003estatus === $status-\u003estatus;\n    }\n}\n```\n\n```php\nfinal class Subscription\n{\n    private Status $status;\n\n    private \\DateTimeImmutable $createdAt;\n\n    public function __construct(\\DateTimeImmutable $createdAt)\n    {\n        $this-\u003estatus = Status::new();\n        $this-\u003ecreatedAt = $createdAt;\n    }\n\n    public function suspend(SuspendingPolicyInterface $suspendingPolicy, \\DateTimeImmutable $at): bool\n    {\n        $result = $suspendingPolicy-\u003esuspend($this, $at);\n        if ($result) {\n            $this-\u003estatus = Status::suspended();\n        }\n\n        return $result;\n    }\n\n    public function isOlderThan(\\DateTimeImmutable $date): bool\n    {\n        return $this-\u003ecreatedAt \u003c $date;\n    }\n\n    public function activate(): void\n    {\n        $this-\u003estatus = Status::active();\n    }\n\n    public function expire(): void\n    {\n        $this-\u003estatus = Status::expired();\n    }\n\n    public function isExpired(): bool\n    {\n        return $this-\u003estatus-\u003eisEqual(Status::expired());\n    }\n\n    public function isActive(): bool\n    {\n        return $this-\u003estatus-\u003eisEqual(Status::active());\n    }\n\n    public function isNew(): bool\n    {\n        return $this-\u003estatus-\u003eisEqual(Status::new());\n    }\n\n    public function isSuspended(): bool\n    {\n        return $this-\u003estatus-\u003eisEqual(Status::suspended());\n    }\n}\n```\n\n```php\nfinal class SubscriptionSuspendingTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function suspending_an_expired_subscription_with_cannot_suspend_expired_policy_is_not_possible(): void\n    {\n        $sut = new Subscription(new \\DateTimeImmutable());\n        $sut-\u003eactivate();\n        $sut-\u003eexpire();\n\n        $result = $sut-\u003esuspend(new CannotSuspendExpiredSubscriptionPolicy(), new \\DateTimeImmutable());\n\n        self::assertFalse($result);\n    }\n\n    /**\n     * @test\n     */\n    public function suspending_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible(): void\n    {\n        $sut = new Subscription(new \\DateTimeImmutable());\n\n        $result = $sut-\u003esuspend(new CannotSuspendNewSubscriptionPolicy(), new \\DateTimeImmutable());\n\n        self::assertFalse($result);\n    }\n\n    /**\n     * @test\n     */\n    public function suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void\n    {\n        $sut = new Subscription(new \\DateTimeImmutable());\n        $sut-\u003eactivate();\n\n        $result = $sut-\u003esuspend(new CannotSuspendNewSubscriptionPolicy(), new \\DateTimeImmutable());\n\n        self::assertTrue($result);\n    }\n\n    /**\n     * @test\n     */\n    public function suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void\n    {\n        $sut = new Subscription(new \\DateTimeImmutable());\n        $sut-\u003eactivate();\n\n        $result = $sut-\u003esuspend(new CannotSuspendExpiredSubscriptionPolicy(), new \\DateTimeImmutable());\n\n        self::assertTrue($result);\n    }\n\n    /**\n     * @test\n     */\n    public function suspending_an_subscription_before_a_one_month_is_not_possible(): void\n    {\n        $sut = new Subscription(new \\DateTimeImmutable('2020-01-01'));\n\n        $result = $sut-\u003esuspend(new CanSuspendAfterOneMonthPolicy(), new \\DateTimeImmutable('2020-01-10'));\n\n        self::assertFalse($result);\n    }\n\n    /**\n     * @test\n     */\n    public function suspending_an_subscription_after_a_one_month_is_possible(): void\n    {\n        $sut = new Subscription(new \\DateTimeImmutable('2020-01-01'));\n\n        $result = $sut-\u003esuspend(new CanSuspendAfterOneMonthPolicy(), new \\DateTimeImmutable('2020-02-02'));\n\n        self::assertTrue($result);\n    }\n}\n```\n\n## Humble pattern\n\nHow to properly unit test a class like this?\n\n```php\nclass ApplicationService\n{\n    public function __construct(\n        private readonly OrderRepository $orderRepository,\n        private readonly FormRepository $formRepository\n    ) {}\n\n    public function changeFormStatus(int $orderId): void\n    {\n        $order = $this-\u003eorderRepository-\u003egetById($orderId);\n        $soapResponse = $this-\u003egetSoapClient()-\u003egetStatusByOrderId($orderId);\n        $form = $this-\u003eformRepository-\u003egetByOrderId($orderId);\n        $form-\u003esetStatus($soapResponse['status']);\n        $form-\u003esetModifiedAt(new \\DateTimeImmutable());\n\n        if ($soapResponse['status'] === 'accepted') {\n            $order-\u003esetStatus('paid');\n        }\n\n        $this-\u003eformRepository-\u003esave($form);\n        $this-\u003eorderRepository-\u003esave($order);\n    }\n\n    private function getSoapClient(): \\SoapClient\n    {\n        return new \\SoapClient('https://legacy_system.pl/Soap/WebService', []);\n    }\n}\n```\n\n\u003e [!TIP|style:flat|label:GOOD]\n\nIt's required to split up an overcomplicated code to separate classes.\n\n```php\nfinal class ApplicationService\n{\n    public function __construct(\n        private readonly OrderRepositoryInterface $orderRepository,\n        private readonly FormRepositoryInterface $formRepository,\n        private readonly FormApiInterface $formApi,\n        private readonly ChangeFormStatusService $changeFormStatusService\n    ) {}\n\n    public function changeFormStatus(int $orderId): void\n    {\n        $order = $this-\u003eorderRepository-\u003egetById($orderId);\n        $form = $this-\u003eformRepository-\u003egetByOrderId($orderId);\n        $status = $this-\u003eformApi-\u003egetStatusByOrderId($orderId);\n\n        $this-\u003echangeFormStatusService-\u003echangeStatus($order, $form, $status);\n\n        $this-\u003eformRepository-\u003esave($form);\n        $this-\u003eorderRepository-\u003esave($order);\n    }\n}\n```\n\n```php\nfinal class ChangeFormStatusService\n{\n    public function changeStatus(Order $order, Form $form, string $formStatus): void\n    {\n        $status = FormStatus::createFromString($formStatus);\n        $form-\u003echangeStatus($status);\n\n        if ($form-\u003eisAccepted()) {\n            $order-\u003echangeStatus(OrderStatus::paid());\n        }\n    }\n}\n```\n\n```php\nfinal class ChangingFormStatusTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function changing_a_form_status_to_accepted_changes_an_order_status_to_paid(): void\n    {\n        $order = new Order();\n        $form = new Form();\n        $status = 'accepted';\n        $sut = new ChangeFormStatusService();\n\n        $sut-\u003echangeStatus($order, $form, $status);\n\n        self::assertTrue($form-\u003eisAccepted());\n        self::assertTrue($order-\u003eisPaid());\n    }\n\n    /**\n     * @test\n     */\n    public function changing_a_form_status_to_refused_not_changes_an_order_status(): void\n    {\n        $order = new Order();\n        $form = new Form();\n        $status = 'new';\n        $sut = new ChangeFormStatusService();\n\n        $sut-\u003echangeStatus($order, $form, $status);\n\n        self::assertFalse($form-\u003eisAccepted());\n        self::assertFalse($order-\u003eisPaid());\n    }\n}\n```\n\nHowever, ApplicationService probably should be tested by an integration test with only mocked FormApiInterface.\n\n## Trivial test\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n```php\nfinal class Customer\n{\n    public function __construct(private string $name) {}\n\n    public function getName(): string\n    {\n        return $this-\u003ename;\n    }\n\n    public function setName(string $name): void\n    {\n        $this-\u003ename = $name;\n    }\n}\n```\n\n```php\nfinal class CustomerTest extends TestCase\n{\n    public function testSetName(): void\n    {\n        $customer = new Customer('Jack');\n\n        $customer-\u003esetName('John');\n\n        self::assertSame('John', $customer-\u003egetName());\n    }\n}\n```\n\n```php\nfinal class EventSubscriber\n{\n    public static function getSubscribedEvents(): array\n    {\n        return ['event' =\u003e 'onEvent'];\n    }\n\n    public function onEvent(): void\n    {\n\n    }\n}\n```\n\n```php\nfinal class EventSubscriberTest extends TestCase\n{\n    public function testGetSubscribedEvents(): void\n    {\n        $result = EventSubscriber::getSubscribedEvents();\n\n        self::assertSame(['event' =\u003e 'onEvent'], $result);\n    }\n}\n```\n\n\n\u003e [!ATTENTION]\n\u003e Testing the code without any complicated logic is senseless, but also leads to fragile tests.\n\n## Fragile test\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n```php\nfinal class UserRepository\n{\n    public function __construct(\n        private readonly Connection $connection\n    ) {}\n\n    public function getUserNameByEmail(string $email): ?array\n    {\n        return $this\n            -\u003econnection\n            -\u003ecreateQueryBuilder()\n            -\u003efrom('user', 'u')\n            -\u003ewhere('u.email = :email')\n            -\u003esetParameter('email', $email)\n            -\u003eexecute()\n            -\u003efetch();\n    }\n}\n```\n\n```php\nfinal class TestUserRepository extends TestCase\n{\n    public function testGetUserNameByEmail(): void\n    {\n        $email = 'test@test.com';\n        $connection = $this-\u003ecreateMock(Connection::class);\n        $queryBuilder = $this-\u003ecreateMock(QueryBuilder::class);\n        $result = $this-\u003ecreateMock(ResultStatement::class);\n        $userRepository = new UserRepository($connection);\n        $connection\n            -\u003eexpects($this-\u003eonce())\n            -\u003emethod('createQueryBuilder')\n            -\u003ewillReturn($queryBuilder);\n        $queryBuilder\n            -\u003eexpects($this-\u003eonce())\n            -\u003emethod('from')\n            -\u003ewith('user', 'u')\n            -\u003ewillReturn($queryBuilder);\n        $queryBuilder\n            -\u003eexpects($this-\u003eonce())\n            -\u003emethod('where')\n            -\u003ewith('u.email = :email')\n            -\u003ewillReturn($queryBuilder);\n        $queryBuilder\n            -\u003eexpects($this-\u003eonce())\n            -\u003emethod('setParameter')\n            -\u003ewith('email', $email)\n            -\u003ewillReturn($queryBuilder);\n        $queryBuilder\n            -\u003eexpects($this-\u003eonce())\n            -\u003emethod('execute')\n            -\u003ewillReturn($result);\n        $result\n            -\u003eexpects($this-\u003eonce())\n            -\u003emethod('fetch')\n            -\u003ewillReturn(['email' =\u003e $email]);\n\n        $result = $userRepository-\u003egetUserNameByEmail($email);\n\n        self::assertSame(['email' =\u003e $email], $result);\n    }\n}\n```\n\n\u003e [!ATTENTION]\n\u003e Testing repositories in that way leads to fragile tests and then refactoring is tough. To test repositories write integration tests.\n\n## Test fixtures\n\n\u003e [!TIP|style:flat|label:GOOD]\n\n```php\nfinal class GoodTest extends TestCase\n{\n    private SubscriptionFactory $sut;\n\n    public function setUp(): void\n    {\n        $this-\u003esut = new SubscriptionFactory();\n    }\n\n    /**\n     * @test\n     */\n    public function creates_a_subscription_for_a_given_date_range(): void\n    {\n        $result = $this-\u003esut-\u003ecreate(new \\DateTimeImmutable(), new \\DateTimeImmutable('now +1 year'));\n\n        self::assertInstanceOf(Subscription::class, $result);\n    }\n\n    /**\n     * @test\n     */\n    public function throws_an_exception_on_invalid_date_range(): void\n    {\n        $this-\u003eexpectException(CreateSubscriptionException::class);\n        \n        $result = $this-\u003esut-\u003ecreate(new \\DateTimeImmutable('now -1 year'), new \\DateTimeImmutable());\n    }\n}\n```\n\n\u003e [!NOTE]\n\u003e - The best case for using the setUp method will be testing stateless objects.\n\u003e - Any configuration made inside `setUp` couples tests together, and has impact on all tests.\n\u003e - It's better to avoid a shared state between tests and configure the initial state accordingly to test method.\n\u003e - Readability is worse compared to configuration made in the proper test method.\n\n\u003e [!TIP|style:flat|label:BETTER]\n\n```php\nfinal class BetterTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void\n    {\n        $sut = $this-\u003ecreateAnActiveSubscription();\n\n        $result = $sut-\u003esuspend(new CannotSuspendNewSubscriptionPolicy(), new \\DateTimeImmutable());\n\n        self::assertTrue($result);\n    }\n\n    /**\n     * @test\n     */\n    public function suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void\n    {\n        $sut = $this-\u003ecreateAnActiveSubscription();\n\n        $result = $sut-\u003esuspend(new CannotSuspendExpiredSubscriptionPolicy(), new \\DateTimeImmutable());\n\n        self::assertTrue($result);\n    }\n\n    /**\n     * @test\n     */\n    public function suspending_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible(): void\n    {\n        $sut = $this-\u003ecreateANewSubscription();\n\n        $result = $sut-\u003esuspend(new CannotSuspendNewSubscriptionPolicy(), new \\DateTimeImmutable());\n\n        self::assertFalse($result);\n    }\n\n    private function createANewSubscription(): Subscription\n    {\n        return new Subscription(new \\DateTimeImmutable());\n    }\n\n    private function createAnActiveSubscription(): Subscription\n    {\n        $subscription = new Subscription(new \\DateTimeImmutable());\n        $subscription-\u003eactivate();\n        \n        return $subscription;\n    }\n}\n```\n\n\u003e [!NOTE]\n\u003e - This approach improves readability and clarifies the separation (code is more read than written).\n\u003e - Private helpers can be tedious to use in each test method, although they provide explicit intentions.\n\n\u003e To share similar testing objects between multiple test classes use:\n\u003e - [Object mother](#object-mother)\n\u003e - [Builder](#builder)\n\n## General testing anti-patterns\n\n### Exposing private state\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n```php\nfinal class Customer\n{\n    private CustomerType $type;\n\n    private DiscountCalculationPolicyInterface $discountCalculationPolicy;\n\n    public function __construct()\n    {\n        $this-\u003etype = CustomerType::NORMAL();\n        $this-\u003ediscountCalculationPolicy = new NormalDiscountPolicy();\n    }\n\n    public function makeVip(): void\n    {\n        $this-\u003etype = CustomerType::VIP();\n        $this-\u003ediscountCalculationPolicy = new VipDiscountPolicy();\n    }\n\n    public function getCustomerType(): CustomerType\n    {\n        return $this-\u003etype;\n    }\n\n    public function getPercentageDiscount(): int\n    {\n        return $this-\u003ediscountCalculationPolicy-\u003egetPercentageDiscount();\n    }\n}\n```\n\n```php\nfinal class InvalidTest extends TestCase\n{\n    public function testMakeVip(): void\n    {\n        $sut = new Customer();\n        $sut-\u003emakeVip();\n\n        self::assertSame(CustomerType::VIP(), $sut-\u003egetCustomerType());\n    }\n}\n```\n\n\u003e [!TIP|style:flat|label:GOOD]\n\n```php\nfinal class Customer\n{\n    private CustomerType $type;\n\n    private DiscountCalculationPolicyInterface $discountCalculationPolicy;\n\n    public function __construct()\n    {\n        $this-\u003etype = CustomerType::NORMAL();\n        $this-\u003ediscountCalculationPolicy = new NormalDiscountPolicy();\n    }\n\n    public function makeVip(): void\n    {\n        $this-\u003etype = CustomerType::VIP();\n        $this-\u003ediscountCalculationPolicy = new VipDiscountPolicy();\n    }\n\n    public function getPercentageDiscount(): int\n    {\n        return $this-\u003ediscountCalculationPolicy-\u003egetPercentageDiscount();\n    }\n}\n```\n\n```php\nfinal class ValidTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function a_vip_customer_has_a_25_percentage_discount(): void\n    {\n        $sut = new Customer();\n        $sut-\u003emakeVip();\n\n        self::assertSame(25, $sut-\u003egetPercentageDiscount());\n    }\n}\n```\n\n\n\u003e [!ATTENTION]\n\u003e Adding additional production code (e.g. getter getCustomerType()) only to verify the state in tests is a bad practice.\n\u003e It should be verified by another domain significant value (in this case getPercentageDiscount()). Of course, sometimes it can be tough to find another way to verify the operation, and we can be forced to add additional production code to verify correctness in tests, but we should try to avoid that.\n\n### Leaking domain details\n\n```php\nfinal class DiscountCalculator\n{\n    public function calculate(int $isVipFromYears): int\n    {\n        Assert::greaterThanEq($isVipFromYears, 0);\n        return min(($isVipFromYears * 10) + 3, 80);\n    }\n}\n```\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n```php\nfinal class InvalidTest extends TestCase\n{\n    /**\n     * @dataProvider discountDataProvider\n     */\n    public function testCalculate(int $vipDaysFrom, int $expected): void\n    {\n        $sut = new DiscountCalculator();\n\n        self::assertSame($expected, $sut-\u003ecalculate($vipDaysFrom));\n    }\n\n    public function discountDataProvider(): array\n    {\n        return [\n            [0, 0 * 10 + 3], //leaking domain details\n            [1, 1 * 10 + 3],\n            [5, 5 * 10 + 3],\n            [8, 80]\n        ];\n    }\n}\n```\n\u003e [!TIP|style:flat|label:GOOD]\n\n```php\nfinal class ValidTest extends TestCase\n{\n    /**\n     * @dataProvider discountDataProvider\n     */\n    public function testCalculate(int $vipDaysFrom, int $expected): void\n    {\n        $sut = new DiscountCalculator();\n\n        self::assertSame($expected, $sut-\u003ecalculate($vipDaysFrom));\n    }\n\n    public function discountDataProvider(): array\n    {\n        return [\n            [0, 3],\n            [1, 13],\n            [5, 53],\n            [8, 80]\n        ];\n    }\n}\n```\n\n\n\u003e [!NOTE]\n\u003e Don't duplicate the production logic in tests. Just verify results by hardcoded values.\n\n### Mocking concrete classes\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n```php\nclass DiscountCalculator\n{\n    public function calculateInternalDiscount(int $isVipFromYears): int\n    {\n        Assert::greaterThanEq($isVipFromYears, 0);\n        return min(($isVipFromYears * 10) + 3, 80);\n    }\n\n    public function calculateAdditionalDiscountFromExternalSystem(): int\n    {\n        // get data from an external system to calculate a discount\n        return 5;\n    }\n}\n```\n\n```php\nclass OrderService\n{\n    public function __construct(private readonly DiscountCalculator $discountCalculator) {}\n\n    public function getTotalPriceWithDiscount(int $totalPrice, int $vipFromDays): int\n    {\n        $internalDiscount = $this-\u003ediscountCalculator-\u003ecalculateInternalDiscount($vipFromDays);\n        $externalDiscount = $this-\u003ediscountCalculator-\u003ecalculateAdditionalDiscountFromExternalSystem();\n        $discountSum = $internalDiscount + $externalDiscount;\n        return $totalPrice - (int) ceil(($totalPrice * $discountSum) / 100);\n    }\n}\n```\n\n```php\nfinal class InvalidTest extends TestCase\n{\n    /**\n     * @dataProvider orderDataProvider\n     */\n    public function testGetTotalPriceWithDiscount(int $totalPrice, int $vipDaysFrom, int $expected): void\n    {\n        $discountCalculator = $this-\u003ecreatePartialMock(DiscountCalculator::class, ['calculateAdditionalDiscountFromExternalSystem']);\n        $discountCalculator-\u003emethod('calculateAdditionalDiscountFromExternalSystem')-\u003ewillReturn(5);\n        $sut = new OrderService($discountCalculator);\n\n        self::assertSame($expected, $sut-\u003egetTotalPriceWithDiscount($totalPrice, $vipDaysFrom));\n    }\n\n    public function orderDataProvider(): array\n    {\n        return [\n            [1000, 0, 920],\n            [500, 1, 410],\n            [644, 5, 270],\n        ];\n    }\n}\n```\n\n\u003e [!TIP|style:flat|label:GOOD]\n\n\n```php\ninterface ExternalDiscountCalculatorInterface\n{\n    public function calculate(): int;\n}\n```\n\n```php\nfinal class InternalDiscountCalculator\n{\n    public function calculate(int $isVipFromYears): int\n    {\n        Assert::greaterThanEq($isVipFromYears, 0);\n        return min(($isVipFromYears * 10) + 3, 80);\n    }\n}\n```\n\n```php\nfinal class OrderService\n{\n    public function __construct(\n        private readonly InternalDiscountCalculator $discountCalculator,\n        private readonly ExternalDiscountCalculatorInterface $externalDiscountCalculator\n    ) {}\n\n    public function getTotalPriceWithDiscount(int $totalPrice, int $vipFromDays): int\n    {\n        $internalDiscount = $this-\u003ediscountCalculator-\u003ecalculate($vipFromDays);\n        $externalDiscount = $this-\u003eexternalDiscountCalculator-\u003ecalculate();\n        $discountSum = $internalDiscount + $externalDiscount;\n        return $totalPrice - (int) ceil(($totalPrice * $discountSum) / 100);\n    }\n}\n```\n\n```php\nfinal class ValidTest extends TestCase\n{\n    /**\n     * @dataProvider orderDataProvider\n     */\n    public function testGetTotalPriceWithDiscount(int $totalPrice, int $vipDaysFrom, int $expected): void\n    {\n        $externalDiscountCalculator = new class() implements ExternalDiscountCalculatorInterface {\n            public function calculate(): int\n            {\n                return 5;\n            }\n        };\n        $sut = new OrderService(new InternalDiscountCalculator(), $externalDiscountCalculator);\n\n        self::assertSame($expected, $sut-\u003egetTotalPriceWithDiscount($totalPrice, $vipDaysFrom));\n    }\n\n    public function orderDataProvider(): array\n    {\n        return [\n            [1000, 0, 920],\n            [500, 1, 410],\n            [644, 5, 270],\n        ];\n    }\n}\n```\n\n\u003e [!NOTE]\n\u003e The necessity to mock a concrete class to replace a part of its behavior means that this class is probably too complicated and violates the Single Responsibility Principle.\n\n### Testing private methods\n\n```php\nfinal class OrderItem\n{\n    public function __construct(public readonly int $total) {}\n}\n```\n\n```php\nfinal class Order\n{\n    /**\n     * @param OrderItem[] $items\n     * @param int $transportCost\n     */\n    public function __construct(private array $items, private int $transportCost) {}\n\n    public function getTotal(): int\n    {\n        return $this-\u003egetItemsTotal() + $this-\u003etransportCost;\n    }\n\n    private function getItemsTotal(): int\n    {\n        return array_reduce(\n            array_map(fn (OrderItem $item) =\u003e $item-\u003etotal, $this-\u003eitems),\n            fn (int $sum, int $total) =\u003e $sum += $total,\n            0\n        );\n    }\n}\n```\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n\n```php\nfinal class InvalidTest extends TestCase\n{\n    /**\n     * @test\n     * @dataProvider ordersDataProvider\n     */\n    public function get_total_returns_a_total_cost_of_a_whole_order(Order $order, int $expectedTotal): void\n    {\n        self::assertSame($expectedTotal, $order-\u003egetTotal());\n    }\n\n    /**\n     * @test\n     * @dataProvider orderItemsDataProvider\n     */\n    public function get_items_total_returns_a_total_cost_of_all_items(Order $order, int $expectedTotal): void\n    {\n        self::assertSame($expectedTotal, $this-\u003einvokePrivateMethodGetItemsTotal($order));\n    }\n\n    public function ordersDataProvider(): array\n    {\n        return [\n            [new Order([new OrderItem(20), new OrderItem(20), new OrderItem(20)], 15), 75],\n            [new Order([new OrderItem(20), new OrderItem(30), new OrderItem(40)], 0), 90],\n            [new Order([new OrderItem(99), new OrderItem(99), new OrderItem(99)], 9), 306]\n        ];\n    }\n\n    public function orderItemsDataProvider(): array\n    {\n        return [\n            [new Order([new OrderItem(20), new OrderItem(20), new OrderItem(20)], 15), 60],\n            [new Order([new OrderItem(20), new OrderItem(30), new OrderItem(40)], 0), 90],\n            [new Order([new OrderItem(99), new OrderItem(99), new OrderItem(99)], 9), 297]\n        ];\n    }\n\n    private function invokePrivateMethodGetItemsTotal(Order \u0026$order): int\n    {\n        $reflection = new \\ReflectionClass(get_class($order));\n        $method = $reflection-\u003egetMethod('getItemsTotal');\n        $method-\u003esetAccessible(true);\n        return $method-\u003einvokeArgs($order, []);\n    }\n}\n```\n\n\u003e [!TIP|style:flat|label:GOOD]\n\n```php\nfinal class ValidTest extends TestCase\n{\n    /**\n     * @test\n     * @dataProvider ordersDataProvider\n     */\n    public function get_total_returns_a_total_cost_of_a_whole_order(Order $order, int $expectedTotal): void\n    {\n        self::assertSame($expectedTotal, $order-\u003egetTotal());\n    }\n\n    public function ordersDataProvider(): array\n    {\n        return [\n            [new Order([new OrderItem(20), new OrderItem(20), new OrderItem(20)], 15), 75],\n            [new Order([new OrderItem(20), new OrderItem(30), new OrderItem(40)], 0), 90],\n            [new Order([new OrderItem(99), new OrderItem(99), new OrderItem(99)], 9), 306]\n        ];\n    }\n}\n```\n\n\u003e [!ATTENTION]\n\u003e Tests should only verify public API.\n\n### Time as a volatile dependency\n\nThe time is a volatile dependency because it is non-deterministic. Each invocation returns a different result.\n\n\u003e [!WARNING|style:flat|label:BAD]\n\n\n```php\nfinal class Clock\n{\n    public static \\DateTime|null $currentDateTime = null;\n\n    public static function getCurrentDateTime(): \\DateTime\n    {\n        if (null === self::$currentDateTime) {\n            self::$currentDateTime = new \\DateTime();\n        }\n\n        return self::$currentDateTime;\n    }\n\n    public static function set(\\DateTime $dateTime): void\n    {\n        self::$currentDateTime = $dateTime;\n    }\n\n    public static function reset(): void\n    {\n        self::$currentDateTime = null;\n    }\n}\n```\n\n```php\nfinal class Customer\n{\n    private \\DateTime $createdAt;\n\n    public function __construct()\n    {\n        $this-\u003ecreatedAt = Clock::getCurrentDateTime();\n    }\n\n    public function isVip(): bool\n    {\n        return $this-\u003ecreatedAt-\u003ediff(Clock::getCurrentDateTime())-\u003ey \u003e= 1;\n    }\n}\n```\n\n```php\nfinal class InvalidTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function a_customer_registered_more_than_a_one_year_ago_is_a_vip(): void\n    {\n        Clock::set(new \\DateTime('2019-01-01'));\n        $sut = new Customer();\n        Clock::reset(); // you have to remember about resetting the shared state\n\n        self::assertTrue($sut-\u003eisVip());\n    }\n\n    /**\n     * @test\n     */\n    public function a_customer_registered_less_than_a_one_year_ago_is_not_a_vip(): void\n    {\n        Clock::set((new \\DateTime())-\u003esub(new \\DateInterval('P2M')));\n        $sut = new Customer();\n        Clock::reset(); // you have to remember about resetting the shared state\n\n        self::assertFalse($sut-\u003eisVip());\n    }\n}\n```\n\n\u003e [!TIP|style:flat|label:GOOD]\n\n\n```php\ninterface ClockInterface\n{\n    public function getCurrentTime(): \\DateTimeImmutable;\n}\n```\n\n```php\nfinal class Clock implements ClockInterface\n{\n    private function __construct()\n    {\n    }\n\n    public static function create(): self\n    {\n        return new self();\n    }\n\n    public function getCurrentTime(): \\DateTimeImmutable\n    {\n        return new \\DateTimeImmutable();\n    }\n}\n```\n\n```php\nfinal class FixedClock implements ClockInterface\n{\n    private function __construct(private readonly \\DateTimeImmutable $fixedDate) {}\n\n    public static function create(\\DateTimeImmutable $fixedDate): self\n    {\n        return new self($fixedDate);\n    }\n\n    public function getCurrentTime(): \\DateTimeImmutable\n    {\n        return $this-\u003efixedDate;\n    }\n}\n```\n\n```php\nfinal class Customer\n{\n    public function __construct(private readonly \\DateTimeImmutable $createdAt) {}\n\n    public function isVip(\\DateTimeImmutable $currentDate): bool\n    {\n        return $this-\u003ecreatedAt-\u003ediff($currentDate)-\u003ey \u003e= 1;\n    }\n}\n```\n\n```php\nfinal class ValidTest extends TestCase\n{\n    /**\n     * @test\n     */\n    public function a_customer_registered_more_than_a_one_year_ago_is_a_vip(): void\n    {\n        $sut = new Customer(FixedClock::create(new \\DateTimeImmutable('2019-01-01'))-\u003egetCurrentTime());\n\n        self::assertTrue($sut-\u003eisVip(FixedClock::create(new \\DateTimeImmutable('2020-01-02'))-\u003egetCurrentTime()));\n    }\n\n    /**\n     * @test\n     */\n    public function a_customer_registered_less_than_a_one_year_ago_is_not_a_vip(): void\n    {\n        $sut = new Customer(FixedClock::create(new \\DateTimeImmutable('2019-01-01'))-\u003egetCurrentTime());\n\n        self::assertFalse($sut-\u003eisVip(FixedClock::create(new \\DateTimeImmutable('2019-05-02'))-\u003egetCurrentTime()));\n    }\n}\n```\n\n\u003e [!NOTE]\n\u003e The time and random numbers should not be generated directly in the domain code. To test behavior we must have\ndeterministic results, so we need to inject these values into a domain object like in the example above.\n\n## 100% Test Coverage shouldn't be the goal\n\n100% Coverage is not the goal or even is undesirable because if there is 100% coverage, tests probably will be very fragile, which means refactoring will be very hard.\nMutation testing gives better feedback about the quality of tests.\n[Read more](https://sarvendev.com/en/2019/06/mutation-testing-we-are-testing-tests/)\n\n## Recommended books\n\n- [Test Driven Development: By Example / Kent Beck](https://amzn.to/3HImbax)  - the classic\n- [Unit Testing Principles, Practices, and Patterns / Vladimir Khorikov](https://amzn.to/3PCMD7f) - the best book about tests I've ever read\n\n## Author\n:construction_worker: **Kamil Ruczyński**\n\n**Twitter:** [https://twitter.com/Sarvendev](https://twitter.com/Sarvendev)  \n**Blog:** [https://sarvendev.com/](https://sarvendev.com/)  \n**LinkedIn:** [https://www.linkedin.com/in/kamilruczynski/](https://www.linkedin.com/in/kamilruczynski/)  \n","funding_links":["https://www.buymeacoffee.com/sarvendev"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsarven%2Funit-testing-tips","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsarven%2Funit-testing-tips","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsarven%2Funit-testing-tips/lists"}