{"id":320522,"date":"2021-03-30T15:00:12","date_gmt":"2021-03-30T15:00:12","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=320522"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=320522","title":{"rendered":"\u042e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0430 PHP \u0432 \u043f\u0440\u0438\u043c\u0435\u0440\u0430\u0445"},"content":{"rendered":"\n<div class=\"post__text post__text-html post__text_v1\" id=\"post-content-body\">\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/1-\/oj\/3d\/1-oj3d2j1lkbnzrb2hbxsr5zreo.jpeg\"><\/div>\n<p>  \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u044b \u043d\u0435\u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u044b. \u0414\u0443\u043c\u0430\u044e, \u043e\u043d\u0438 \u0435\u0441\u0442\u044c \u0432 \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u0435 \u0438\u0437 \u043d\u0435\u0434\u0430\u0432\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432. \u042e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u044b \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0432\u0430\u0436\u043d\u0435\u0439\u0448\u0438\u043c\u0438 \u0432 enterprise-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f\u0445 \u0441 \u043e\u0431\u0438\u043b\u0438\u0435\u043c \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0438, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043e\u043d\u0438 \u0431\u044b\u0441\u0442\u0440\u044b\u0435 \u0438 \u043c\u043e\u0433\u0443\u0442 \u0441\u0440\u0430\u0437\u0443 \u0441\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u043c, \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u0430 \u043b\u0438 \u043d\u0430\u0448\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f. \u041e\u0434\u043d\u0430\u043a\u043e \u044f \u0447\u0430\u0441\u0442\u043e \u0441\u0442\u0430\u043b\u043a\u0438\u0432\u0430\u044e\u0441\u044c \u0441 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u043c\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u044b \u0441 \u0445\u043e\u0440\u043e\u0448\u0438\u043c\u0438 \u0442\u0435\u0441\u0442\u0430\u043c\u0438, \u0445\u043e\u0442\u044f \u0442\u0435 \u0438 \u043a\u0440\u0430\u0439\u043d\u0435 \u043f\u043e\u043b\u0435\u0437\u043d\u044b. \u042f \u0434\u0430\u043c \u0432\u0430\u043c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u043e\u0432\u0435\u0442\u043e\u0432 \u0441 \u043f\u0440\u0438\u043c\u0435\u0440\u0430\u043c\u0438, \u043a\u0430\u043a \u043f\u0438\u0441\u0430\u0442\u044c \u0445\u043e\u0440\u043e\u0448\u0438\u0435 \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u044b.<br \/>  <a name=\"habracut\"><\/a>  <\/p>\n<h2>\u0421\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435<\/h2>\n<p>  <\/p>\n<ol>\n<li><a href=\"#1\">\u0422\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0434\u0443\u0431\u043b\u0438<\/a><\/li>\n<li><a href=\"#2\">\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u044f<\/a><\/li>\n<li><a href=\"#3\">\u0428\u0430\u0431\u043b\u043e\u043d AAA<\/a><\/li>\n<li><a href=\"#4\">\u041c\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442\u0430<\/a><\/li>\n<li><a href=\"#5\">\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0442\u0435\u0441\u0442<\/a><\/li>\n<li><a href=\"#6\">\u0414\u0432\u0435 \u0448\u043a\u043e\u043b\u044b \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f<\/a><br \/> \n<ul>\n<li><a href=\"#7\">\u041a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0430\u044f<\/a><\/li>\n<li><a href=\"#8\">\u041c\u043e\u043a\u043e\u0432\u0430\u044f<\/a><\/li>\n<li><a href=\"#9\">\u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#10\">\u041c\u043e\u043a\u0438 \u0438 \u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0438<\/a><\/li>\n<li><a href=\"#11\">\u0422\u0440\u0438 \u0441\u0442\u0438\u043b\u044f \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f<\/a><br \/> \n<ul>\n<li><a href=\"#12\">\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442<\/a><\/li>\n<li><a href=\"#13\">\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435<\/a><\/li>\n<li><a href=\"#14\">\u0412\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#15\">\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u0430\u044f \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0438 \u0442\u0435\u0441\u0442\u044b<\/a><\/li>\n<li><a href=\"#16\">\u041d\u0430\u0431\u043b\u044e\u0434\u0430\u0435\u043c\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0438 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438<\/a><\/li>\n<li><a href=\"#17\">\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f<\/a><\/li>\n<li><a href=\"#18\">\u0428\u0430\u0431\u043b\u043e\u043d humble<\/a><\/li>\n<li><a href=\"#19\">\u0411\u0435\u0441\u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0439 \u0442\u0435\u0441\u0442<\/a><\/li>\n<li><a href=\"#20\">\u0425\u0440\u0443\u043f\u043a\u0438\u0439 \u0442\u0435\u0441\u0442<\/a><\/li>\n<li><a href=\"#21\">\u0418\u0441\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432<\/a><\/li>\n<li><a href=\"#22\">\u041e\u0431\u0449\u0438\u0435 \u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f<\/a><br \/> \n<ul>\n<li><a href=\"#23\">\u0420\u0430\u0441\u043a\u0440\u044b\u0442\u0438\u0435 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f<\/a><\/li>\n<li><a href=\"#24\">\u0423\u0442\u0435\u0447\u043a\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0435\u0439 \u043e \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438<\/a><\/li>\n<li><a href=\"#25\">\u041c\u043e\u043a\u0438\u043d\u0433 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u043a\u043b\u0430\u0441\u0441\u043e\u0432<\/a><\/li>\n<li><a href=\"#26\">\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432<\/a><\/li>\n<li><a href=\"#27\">\u0412\u0440\u0435\u043c\u044f \u043a\u0430\u043a \u043d\u0435\u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u0430\u044f \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#28\">\u041d\u0435 \u0433\u043e\u043d\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043b\u043d\u044b\u043c \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435\u043c<\/a><\/li>\n<li><a href=\"#29\">\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c\u044b\u0435 \u043a\u043d\u0438\u0433\u0438<\/a><\/li>\n<\/ol>\n<p>  <a name=\"1\"><\/a><\/p>\n<h2>\u0422\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0434\u0443\u0431\u043b\u0438<\/h2>\n<p>  \u042d\u0442\u043e \u0444\u0430\u043b\u044c\u0448\u0438\u0432\u044b\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0432 \u0442\u0435\u0441\u0442\u0430\u0445.<\/p>\n<h3>\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0438 (Stub)<\/h3>\n<p>  <\/p>\n<h4>\u0418\u043c\u0438\u0442\u0430\u0442\u043e\u0440 (Dummy)<\/h4>\n<p>  \u0418\u043c\u0438\u0442\u0430\u0442\u043e\u0440 \u2014 \u0432\u0441\u0435\u0433\u043e \u043b\u0438\u0448\u044c \u043f\u0440\u043e\u0441\u0442\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u0434\u0435\u043b\u0430\u0435\u0442.<\/p>\n<pre><code class=\"php\">final class Mailer implements MailerInterface {     public function send(Message $message): void     {     } } <\/code><\/pre>\n<p>  <\/p>\n<h4>\u0424\u0430\u043b\u044c\u0448\u0438\u0432\u043a\u0430 (Fake)<\/h4>\n<p>  \u0424\u0430\u043b\u044c\u0448\u0438\u0432\u043a\u0430 \u2014 \u044d\u0442\u043e \u0443\u043f\u0440\u043e\u0449\u0451\u043d\u043d\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f, \u044d\u043c\u0443\u043b\u0438\u0440\u0443\u044e\u0449\u0430\u044f \u043d\u0443\u0436\u043d\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435.<\/p>\n<pre><code class=\"php\">final class InMemoryCustomerRepository implements CustomerRepositoryInterface {     \/**      * @var Customer[]      *\/     private array $customers;      public function __construct()     {         $this-&gt;customers = [];     }      public function store(Customer $customer): void     {         $this-&gt;customers[(string) $customer-&gt;id()-&gt;id()] = $customer;     }      public function get(CustomerId $id): Customer     {         if (!isset($this-&gt;customers[(string) $id-&gt;id()])) {             throw new CustomerNotFoundException();         }          return $this-&gt;customers[(string) $id-&gt;id()];     }      public function findByEmail(Email $email): Customer     {         foreach ($this-&gt;customers as $customer) {             if ($customer-&gt;getEmail()-&gt;isEqual($email)) {                 return $customer;             }         }          throw new CustomerNotFoundException();     } } <\/code><\/pre>\n<p>  <\/p>\n<h4>\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430 (Stub)<\/h4>\n<p>  \u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430 \u2014 \u044d\u0442\u043e \u043f\u0440\u043e\u0441\u0442\u0435\u0439\u0448\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0441 \u043f\u0440\u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u043c \u0432 \u043a\u043e\u0434\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435\u043c.<\/p>\n<pre><code class=\"php\">final class UniqueEmailSpecificationStub implements UniqueEmailSpecificationInterface {     public function isUnique(Email $email): bool     {         return true;     } } $specificationStub = $this-&gt;createStub(UniqueEmailSpecificationInterface::class); $specificationStub-&gt;method('isUnique')-&gt;willReturn(true); <\/code><\/pre>\n<p>  <\/p>\n<h3>\u041c\u043e\u043a\u0438 (Mock)<\/h3>\n<p>  <\/p>\n<h4>\u0428\u043f\u0438\u043e\u043d (Spy)<\/h4>\n<p>  \u0428\u043f\u0438\u043e\u043d \u2014 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f.<\/p>\n<pre><code class=\"php\">final class Mailer implements MailerInterface {     \/**      * @var Message[]      *\/     private array $messages;          public function __construct()     {         $this-&gt;messages = [];     }      public function send(Message $message): void     {         $this-&gt;messages[] = $message;     }      public function getCountOfSentMessages(): int     {         return count($this-&gt;messages);     } } <\/code><\/pre>\n<p>  <\/p>\n<h4>\u041c\u043e\u043a (Mock)<\/h4>\n<p>  \u041c\u043e\u043a \u2014 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0438\u043c\u0438\u0442\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0432\u044b\u0437\u043e\u0432\u043e\u0432 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432.<\/p>\n<pre><code class=\"php\">$message = new Message('test@test.com', 'Test', 'Test test test'); $mailer = $this-&gt;createMock(MailerInterface::class); $mailer     -&gt;expects($this-&gt;once())     -&gt;method('send')     -&gt;with($this-&gt;equalTo($message)); <\/code><\/pre>\n<p>  ! \u0414\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0439 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0443, \u0430 \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u2014 \u043c\u043e\u043a. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043e\u0431 \u044d\u0442\u043e\u043c \u0432 \u0433\u043b\u0430\u0432\u0435<a href=\"https:\/\/github.com\/sarven\/unit-testing-tips#mock-vs-stub\"> \u041c\u043e\u043a\u0438 \u0438 \u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0438<\/a><u>.<\/u><\/p>\n<p>  <a name=\"2\"><\/a><\/p>\n<h2>\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u044f<\/h2>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">public function test(): void {     $subscription = SubscriptionMother::new();      $subscription-&gt;activate();      self::assertSame(Status::activated(), $subscription-&gt;status()); } <\/code><\/pre>\n<p>  <strong>\u042f\u0432\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0439\u0442\u0435, \u0447\u0442\u043e \u0432\u044b \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u0442\u0435.<\/strong><\/p>\n<pre><code class=\"php\">public function sut(): void {     \/\/ sut = System under test     $sut = SubscriptionMother::new();      $sut-&gt;activate();      self::assertSame(Status::activated(), $sut-&gt;status()); } <\/code><\/pre>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">public function it_throws_invalid_credentials_exception_when_sign_in_with_invalid_credentials(): void {  }  public function testCreatingWithATooShortPasswordIsNotPossible(): void {  }  public function testDeactivateASubscription(): void {  } <\/code><\/pre>\n<p>  \u041b\u0443\u0447\u0448\u0435:<\/p>\n<ul>\n<li>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0438\u0436\u043d\u0438\u0445 \u043f\u043e\u0434\u0447\u0451\u0440\u043a\u0438\u0432\u0430\u043d\u0438\u0439 \u043f\u043e\u0432\u044b\u0448\u0430\u0435\u0442 \u0443\u0434\u043e\u0431\u043e\u0447\u0438\u0442\u0430\u0435\u043c\u043e\u0441\u0442\u044c.<\/li>\n<li>\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435, \u0430 \u043d\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e.<\/li>\n<li>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u044f \u0431\u0435\u0437 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0442\u0435\u0440\u043c\u0438\u043d\u043e\u0432. \u041e\u043d\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u043e\u043d\u044f\u0442\u043d\u044b \u043d\u0435\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442\u0430\u043c.<\/li>\n<\/ul>\n<p>  <\/p>\n<pre><code class=\"php\">public function sign_in_with_invalid_credentials_is_not_possible(): void {  }  public function creating_with_a_too_short_password_is_not_possible(): void {  }  public function deactivating_an_activated_subscription_is_valid(): void {  }  public function deactivating_an_inactive_subscription_is_invalid(): void {  } <\/code><\/pre>\n<p>  \u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u0432\u0430\u0436\u043d\u043e \u043f\u0440\u0438 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043d\u044b\u0445 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0435\u0432. \u0415\u0441\u043b\u0438 \u0432\u0430\u0448 \u043a\u043e\u0434 \u0443\u0442\u0438\u043b\u0438\u0442\u0430\u0440\u043d\u044b\u0439, \u0442\u043e \u044d\u0442\u043e \u0443\u0436\u0435 \u043d\u0435 \u0442\u0430\u043a \u0432\u0430\u0436\u043d\u043e.<\/p>\n<p>  \u041f\u043e\u0447\u0435\u043c\u0443 \u0432\u0430\u0436\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u043d\u0435\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442\u044b \u043c\u043e\u0433\u043b\u0438 \u0447\u0438\u0442\u0430\u0442\u044c \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u044b? \u0415\u0441\u043b\u0438 \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0441\u043b\u043e\u0436\u043d\u0430\u044f \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043d\u0430\u044f \u043b\u043e\u0433\u0438\u043a\u0430, \u0442\u043e \u044d\u0442\u0430 \u043b\u043e\u0433\u0438\u043a\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u043e\u0447\u0435\u0432\u0438\u0434\u043d\u0430 \u0434\u043b\u044f \u0432\u0441\u0435\u0445, \u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0442\u0435\u0441\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u0431\u0435\u0437 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0442\u0435\u0440\u043c\u0438\u043d\u043e\u0432, \u0447\u0442\u043e\u0431\u044b \u0432\u044b \u043c\u043e\u0433\u043b\u0438 \u0433\u043e\u0432\u043e\u0440\u0438\u0442\u044c \u0441 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u0435\u043b\u044f\u043c\u0438 \u0431\u0438\u0437\u043d\u0435\u0441\u0430 \u043d\u0430 \u0442\u043e\u043c \u0436\u0435 \u044f\u0437\u044b\u043a\u0435, \u0447\u0442\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u0442\u0435\u0441\u0442\u0430\u0445. \u041e\u0441\u0432\u043e\u0431\u043e\u0434\u0438\u0442\u0435 \u043e\u0442 \u0442\u0435\u0440\u043c\u0438\u043d\u043e\u0432 \u0438 \u0432\u0435\u0441\u044c \u043a\u043e\u0434, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0439 \u0441 \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043d\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u044c\u044e, \u0438\u043d\u0430\u0447\u0435 \u043d\u0435\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442\u044b \u043d\u0435 \u0441\u043c\u043e\u0433\u0443\u0442 \u043f\u043e\u043d\u044f\u0442\u044c \u044d\u0442\u0438 \u0442\u0435\u0441\u0442\u044b. \u041d\u0435 \u043d\u0430\u0434\u043e \u043f\u0438\u0441\u0430\u0442\u044c \u0432 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u044f\u0445 \u00ab\u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 null\u00bb, \u00ab\u0431\u0440\u043e\u0441\u0430\u0435\u0442 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u00bb \u0438 \u0442.\u0434. \u0422\u0430\u043a\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043d\u0435 \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0441\u044f \u043a \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043d\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438.<\/p>\n<p>  <a name=\"3\"><\/a><\/p>\n<h2>\u0428\u0430\u0431\u043b\u043e\u043d AAA<\/h2>\n<p>  \u0422\u0430\u043a\u0436\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043d \u043a\u0430\u043a \u00abGiven, When, Then\u00bb.<\/p>\n<p>  \u0412\u044b\u0434\u0435\u043b\u044f\u0439\u0442\u0435 \u0432 \u0442\u0435\u0441\u0442\u0430\u0445 \u0442\u0440\u0438 \u044d\u0442\u0430\u043f\u0430:<\/p>\n<ul>\n<li><strong>Arrange<\/strong>: \u043f\u0440\u0438\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u0443\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u043a \u043d\u0443\u0436\u043d\u043e\u043c\u0443 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e. \u041f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u044c\u0442\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438, \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b, \u0438 \u0441\u043e\u0437\u0434\u0430\u0439\u0442\u0435 SUT.<\/li>\n<li><strong>Act<\/strong>: \u0438\u0437\u0432\u043b\u0435\u043a\u0438\u0442\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u044b\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442.<\/li>\n<li><strong>Assert<\/strong>: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442, \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0438\u043b\u0438 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c\u0438.<\/li>\n<\/ul>\n<p>  <\/p>\n<pre><code class=\"php\">public function aaa_pattern_example_test(): void {     \/\/Arrange|Given     $sut = SubscriptionMother::new();      \/\/Act|When     $sut-&gt;activate();      \/\/Assert|Then     self::assertSame(Status::activated(), $sut-&gt;status()); } <\/code><\/pre>\n<p>  <a name=\"4\"><\/a><\/p>\n<h2>\u041c\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442\u0430<\/h2>\n<p>  \u042d\u0442\u043e\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u043f\u043e\u043c\u043e\u0433\u0430\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0442\u0435\u0441\u0442\u0430\u0445. \u0411\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044f \u044d\u0442\u043e\u043c\u0443 \u044d\u0442\u0430\u043f \u00abarrange\u00bb \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f \u043a\u0440\u0430\u0442\u043a\u0438\u043c, \u0430 \u0432\u0435\u0441\u044c \u0442\u0435\u0441\u0442 \u2014 \u0431\u043e\u043b\u0435\u0435 \u0443\u0434\u043e\u0431\u043e\u0447\u0438\u0442\u0430\u0435\u043c\u044b\u043c.<\/p>\n<pre><code class=\"php\">final class SubscriptionMother {     public static function new(): Subscription     {         return new Subscription();     }      public static function activated(): Subscription     {         $subscription = new Subscription();         $subscription-&gt;activate();         return $subscription;     }      public static function deactivated(): Subscription     {         $subscription = self::activated();         $subscription-&gt;deactivate();         return $subscription;     } } final class ExampleTest {     public function example_test_with_activated_subscription(): void     {         $activatedSubscription = SubscriptionMother::activated();          \/\/ do something          \/\/ check something     }      public function example_test_with_deactivated_subscription(): void     {         $deactivatedSubscription = SubscriptionMother::deactivated();          \/\/ do something          \/\/ check something     } } <\/code><\/pre>\n<p>  <a name=\"5\"><\/a><\/p>\n<h2>\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0442\u0435\u0441\u0442<\/h2>\n<p>  \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0442\u0435\u0441\u0442 \u2014 \u0445\u043e\u0440\u043e\u0448\u0438\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f SUT \u0441 \u043c\u043d\u043e\u0433\u043e\u0447\u0438\u0441\u043b\u0435\u043d\u043d\u044b\u043c\u0438 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043c\u0438 \u0431\u0435\u0437 \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u044f \u043a\u043e\u0434\u0430. \u041d\u043e \u0442\u0430\u043a\u0438\u0435 \u0442\u0435\u0441\u0442\u044b \u043c\u0435\u043d\u0435\u0435 \u0443\u0434\u043e\u0431\u043e\u0447\u0438\u0442\u0430\u0435\u043c\u044b\u0435. \u0427\u0442\u043e\u0431\u044b \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u0443\u043b\u0443\u0447\u0448\u0438\u0442\u044c \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044e, \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0438 \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u044b \u043d\u0443\u0436\u043d\u043e \u0440\u0430\u0441\u043a\u0438\u0434\u0430\u0442\u044c \u043f\u043e \u0440\u0430\u0437\u043d\u044b\u043c \u0442\u0435\u0441\u0442\u0430\u043c.<\/p>\n<pre><code class=\"php\">final class ExampleTest extends TestCase {     \/**      * @test      * @dataProvider getInvalidEmails      *\/     public function detects_an_invalid_email_address(string $email): void     {         $sut = new EmailValidator();          $result = $sut-&gt;isValid($email);          self::assertFalse($result);     }      \/**      * @test      * @dataProvider getValidEmails      *\/     public function detects_an_valid_email_address(string $email): void     {         $sut = new EmailValidator();          $result = $sut-&gt;isValid($email);          self::assertTrue($result);     }      public function getInvalidEmails(): array     {         return [             ['test'],             ['test@'],             ['test@test'],             \/\/...         ];     }      public function getValidEmails(): array     {         return [             ['test@test.com'],             ['test123@test.com'],             ['Test123@test.com'],             \/\/...         ];     } } <\/code><\/pre>\n<p>  <a name=\"6\"><\/a><\/p>\n<h2>\u0414\u0432\u0435 \u0448\u043a\u043e\u043b\u044b \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f<\/h2>\n<p>  <a name=\"7\"><\/a><\/p>\n<h3>\u041a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0430\u044f (\u0414\u0435\u0442\u0440\u043e\u0439\u0442\u0441\u043a\u0430\u044f \u0448\u043a\u043e\u043b\u0430)<\/h3>\n<p>  <\/p>\n<ul>\n<li>\u041c\u043e\u0434\u0443\u043b\u044c \u2014 \u044d\u0442\u043e \u0435\u0434\u0438\u043d\u0438\u0446\u0430 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f, \u043c\u043e\u0436\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0432\u0437\u0430\u0438\u043c\u043e\u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u043a\u043b\u0430\u0441\u0441\u043e\u0432.<\/li>\n<li>\u0412\u0441\u0435 \u0442\u0435\u0441\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0438\u0437\u043e\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u044b \u0434\u0440\u0443\u0433 \u043e\u0442 \u0434\u0440\u0443\u0433\u0430. \u0414\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0432\u044b\u0437\u044b\u0432\u0430\u0442\u044c \u0438\u0445 \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e \u0438\u043b\u0438 \u0432 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u043e\u043c \u043f\u043e\u0440\u044f\u0434\u043a\u0435.<\/li>\n<\/ul>\n<p>  <\/p>\n<pre><code class=\"php\">final class TestExample extends TestCase {     \/**      * @test      *\/     public function suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void     {         $canAlwaysSuspendPolicy = new CanAlwaysSuspendPolicy();         $sut = new Subscription();          $result = $sut-&gt;suspend($canAlwaysSuspendPolicy);          self::assertTrue($result);         self::assertSame(Status::suspend(), $sut-&gt;status());     } } <\/code><\/pre>\n<p>  <a name=\"8\"><\/a><\/p>\n<h3>\u041c\u043e\u043a\u043e\u0432\u0430\u044f (\u041b\u043e\u043d\u0434\u043e\u043d\u0441\u043a\u0430\u044f \u0448\u043a\u043e\u043b\u0430)<\/h3>\n<p>  <\/p>\n<ul>\n<li>\u041c\u043e\u0434\u0443\u043b\u044c \u2014 \u044d\u0442\u043e \u043e\u0434\u0438\u043d \u043a\u043b\u0430\u0441\u0441.<\/li>\n<li>\u041c\u043e\u0434\u0443\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0438\u0437\u043e\u043b\u0438\u0440\u043e\u0432\u0430\u043d \u043e\u0442 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432.<\/li>\n<\/ul>\n<p>  <\/p>\n<pre><code class=\"php\">final class TestExample extends TestCase {     \/**      * @test      *\/     public function suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void     {         $canAlwaysSuspendPolicy = $this-&gt;createStub(SuspendingPolicyInterface::class);         $canAlwaysSuspendPolicy-&gt;method('suspend')-&gt;willReturn(true);         $sut = new Subscription();          $result = $sut-&gt;suspend($canAlwaysSuspendPolicy);          self::assertTrue($result);         self::assertSame(Status::suspend(), $sut-&gt;status());     } } <\/code><\/pre>\n<p>  \u041a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043b\u0443\u0447\u0448\u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u0445\u0440\u0443\u043f\u043a\u0438\u0445 \u0442\u0435\u0441\u0442\u043e\u0432.<\/p>\n<p>  <a name=\"9\"><\/a><\/p>\n<h2>\u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438<\/h2>\n<p>  [TODO]<\/p>\n<p>  <a name=\"10\"><\/a><\/p>\n<h2>\u041c\u043e\u043a\u0438 \u0438 \u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0438<\/h2>\n<p>  \u041f\u0440\u0438\u043c\u0435\u0440:<\/p>\n<pre><code class=\"php\">final class NotificationService {     public function __construct(         private MailerInterface $mailer,         private MessageRepositoryInterface $messageRepository     ) {}      public function send(): void     {         $messages = $this-&gt;messageRepository-&gt;getAll();         foreach ($messages as $message) {             $this-&gt;mailer-&gt;send($message);         }     } } <\/code><\/pre>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<ul>\n<li><strong>\u041f\u0440\u043e\u0432\u0435\u0440\u043e\u0447\u043d\u044b\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 \u0437\u0430\u0433\u043b\u0443\u0448\u043a\u0430\u043c\u0438 \u043f\u0440\u0438\u0432\u043e\u0434\u044f\u0442 \u043a \u0445\u0440\u0443\u043f\u043a\u0438\u043c \u0442\u0435\u0441\u0442\u0430\u043c.<\/strong><\/li>\n<\/ul>\n<p>  <\/p>\n<pre><code class=\"php\">final class TestExample extends TestCase {     \/**      * @test      *\/     public function sends_all_notifications(): void     {         $message1 = new Message();         $message2 = new Message();         $messageRepository = $this-&gt;createMock(MessageRepositoryInterface::class);         $messageRepository-&gt;method('getAll')-&gt;willReturn([$message1, $message2]);         $mailer = $this-&gt;createMock(MailerInterface::class);         $sut = new NotificationService($mailer, $messageRepository);          $messageRepository-&gt;expects(self::once())-&gt;method('getAll');         $mailer-&gt;expects(self::exactly(2))-&gt;method('send')             -&gt;withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);          $sut-&gt;send();     } } <\/code><\/pre>\n<p>  \u0425\u043e\u0440\u043e\u0448\u043e:<\/p>\n<pre><code class=\"php\">final class TestExample extends TestCase {     \/**      * @test      *\/     public function sends_all_notifications(): void     {         $message1 = new Message();         $message2 = new Message();         $messageRepository = $this-&gt;createStub(MessageRepositoryInterface::class);         $messageRepository-&gt;method('getAll')-&gt;willReturn([$message1, $message2]);         $mailer = $this-&gt;createMock(MailerInterface::class);         $sut = new NotificationService($mailer, $messageRepository);          \/\/ Removed asserting interactions with the stub         $mailer-&gt;expects(self::exactly(2))-&gt;method('send')             -&gt;withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);          $sut-&gt;send();     } } <\/code><\/pre>\n<p>  <a name=\"11\"><\/a><\/p>\n<h2>\u0422\u0440\u0438 \u0441\u0442\u0438\u043b\u044f \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f<\/h2>\n<p>  <a name=\"12\"><\/a><\/p>\n<h3>\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442<\/h3>\n<p>  \u041b\u0443\u0447\u0448\u0438\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442:<\/p>\n<ul>\n<li><strong>\u041d\u0430\u0438\u043b\u0443\u0447\u0448\u0430\u044f \u0441\u043e\u043f\u0440\u043e\u0442\u0438\u0432\u043b\u044f\u0435\u043c\u043e\u0441\u0442\u044c \u0440\u0435\u0444\u0430\u043a\u0442\u043e\u0440\u0438\u043d\u0433\u0443.<\/strong><\/li>\n<li><strong>\u041d\u0430\u0438\u043b\u0443\u0447\u0448\u0430\u044f \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c.<\/strong><\/li>\n<li><strong>\u041c\u0435\u043d\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0443\u0441\u0438\u043b\u0438\u0439 \u043f\u043e \u0441\u043e\u043f\u0440\u043e\u0432\u043e\u0436\u0434\u0435\u043d\u0438\u044e.<\/strong><\/li>\n<li><strong>\u0415\u0441\u043b\u0438 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u0432\u0438\u0434 \u0442\u0435\u0441\u0442\u043e\u0432.<\/strong><\/li>\n<\/ul>\n<p>  <\/p>\n<pre><code class=\"php\">final class ExampleTest extends TestCase {     \/**      * @test      * @dataProvider getInvalidEmails      *\/     public function detects_an_invalid_email_address(string $email): void     {         $sut = new EmailValidator();          $result = $sut-&gt;isValid($email);          self::assertFalse($result);     }      \/**      * @test      * @dataProvider getValidEmails      *\/     public function detects_an_valid_email_address(string $email): void     {         $sut = new EmailValidator();          $result = $sut-&gt;isValid($email);          self::assertTrue($result);     }      public function getInvalidEmails(): array     {         return [             ['test'],             ['test@'],             ['test@test'],             \/\/...         ];     }      public function getValidEmails(): array     {         return [             ['test@test.com'],             ['test123@test.com'],             ['Test123@test.com'],             \/\/...         ];     } } <\/code><\/pre>\n<p>  <a name=\"13\"><\/a><\/p>\n<h3>\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435<\/h3>\n<p>  \u0412\u0430\u0440\u0438\u0430\u043d\u0442 \u043f\u043e\u0445\u0443\u0436\u0435:<\/p>\n<ul>\n<li><strong>\u0425\u0443\u0436\u0435 \u0441\u043e\u043f\u0440\u043e\u0442\u0438\u0432\u043b\u044f\u0435\u043c\u043e\u0441\u0442\u044c \u0440\u0435\u0444\u0430\u043a\u0442\u043e\u0440\u0438\u043d\u0433\u0443.<\/strong><\/li>\n<li><strong>\u0425\u0443\u0436\u0435 \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c.<\/strong><\/li>\n<li><strong>\u0421\u043b\u043e\u0436\u043d\u0435\u0435 \u0432 \u0441\u043e\u043f\u0440\u043e\u0432\u043e\u0436\u0434\u0435\u043d\u0438\u0438.<\/strong><\/li>\n<\/ul>\n<p>  <\/p>\n<pre><code class=\"php\">final class ExampleTest extends TestCase {     \/**      * @test      *\/     public function adding_an_item_to_cart(): void     {         $item = new CartItem('Product');         $sut = new Cart();          $sut-&gt;addItem($item);          self::assertSame(1, $sut-&gt;getCount());         self::assertSame($item, $sut-&gt;getItems()[0]);     } } <\/code><\/pre>\n<p>  <a name=\"14\"><\/a><\/p>\n<h3>\u0412\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435<\/h3>\n<p>  \u0425\u0443\u0434\u0448\u0438\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442:<\/p>\n<ul>\n<li><strong>\u0425\u0443\u0434\u0448\u0430\u044f \u0441\u043e\u043f\u0440\u043e\u0442\u0438\u0432\u043b\u044f\u0435\u043c\u043e\u0441\u0442\u044c \u0440\u0435\u0444\u0430\u043a\u0442\u043e\u0440\u0438\u043d\u0433\u0443.<\/strong><\/li>\n<li><strong>\u0425\u0443\u0434\u0448\u0430\u044f \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c.<\/strong><\/li>\n<li><strong>\u0421\u043b\u043e\u0436\u043d\u0435\u0435 \u0432\u0441\u0435\u0433\u043e \u0432 \u0441\u043e\u043f\u0440\u043e\u0432\u043e\u0436\u0434\u0435\u043d\u0438\u0438.<\/strong><\/li>\n<\/ul>\n<p>  <\/p>\n<pre><code class=\"php\">final class ExampleTest extends TestCase {     \/**      * @test      *\/     public function sends_all_notifications(): void     {         $message1 = new Message();         $message2 = new Message();         $messageRepository = $this-&gt;createStub(MessageRepositoryInterface::class);         $messageRepository-&gt;method('getAll')-&gt;willReturn([$message1, $message2]);         $mailer = $this-&gt;createMock(MailerInterface::class);         $sut = new NotificationService($mailer, $messageRepository);          $mailer-&gt;expects(self::exactly(2))-&gt;method('send')             -&gt;withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);          $sut-&gt;send();     } } <\/code><\/pre>\n<p>  <a name=\"15\"><\/a><\/p>\n<h2>\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u0430\u044f \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0438 \u0442\u0435\u0441\u0442\u044b<\/h2>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">final class NameService {     public function __construct(private 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[] = new Name($nameData[0], new Gender($nameData[1]));         }          $this-&gt;cacheStorage-&gt;store('names', $names);     } } <\/code><\/pre>\n<p>  <strong>\u041a\u0430\u043a \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0439 \u043a\u043e\u0434? \u042d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0442\u0435\u0441\u0442\u043e\u0432, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043e\u043d\u0438 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043d\u044b\u0439 \u043a\u043e\u0434, \u043e\u0442\u043d\u043e\u0441\u044f\u0449\u0438\u0439\u0441\u044f \u043a \u0444\u0430\u0439\u043b\u043e\u0432\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.<\/strong><\/p>\n<p>  \u0425\u043e\u0440\u043e\u0448\u043e:<\/p>\n<p>  \u041a\u0430\u043a \u0438 \u0432 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0439 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0435, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043e\u0442\u0434\u0435\u043b\u0438\u0442\u044c \u043a\u043e\u0434 \u0441 \u043f\u043e\u0431\u043e\u0447\u043d\u044b\u043c\u0438 \u044d\u0444\u0444\u0435\u043a\u0442\u0430\u043c\u0438 \u043e\u0442 \u043a\u043e\u0434\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043b\u043e\u0433\u0438\u043a\u0443.<\/p>\n<pre><code class=\"php\">final class NameParser {     \/**      * @param array $namesData      * @return Name[]      *\/     public function parse(array $namesData): array     {         $names = [];          foreach ($namesData as $nameData) {             if (!isset($nameData[0], $nameData[1])) {                 continue;             }              $names[] = new Name($nameData[0], new Gender($nameData[1]));         }          return $names;     } } final class CsvNamesFileLoader {     public function load(): array     {         return array_map('str_getcsv', file(__DIR__.'\/..\/names.csv'));     } } final class ApplicationService {     public function __construct(         private CsvNamesFileLoader $fileLoader,         private NameParser $parser,         private CacheStorageInterface $cacheStorage     ) {}      public function loadNames(): void     {         $namesData = $this-&gt;fileLoader-&gt;load();         $names = $this-&gt;parser-&gt;parse($namesData);         $this-&gt;cacheStorage-&gt;store('names', $names);     } } final class ValidUnitExampleTest extends TestCase {     \/**      * @test      *\/     public function parse_all_names(): void     {         $namesData = [             ['John', 'M'],             ['Lennon', 'U'],             ['Sarah', 'W']         ];         $sut = new NameParser();          $result = $sut-&gt;parse($namesData);                  self::assertSame(             [                 new Name('John', new Gender('M')),                 new Name('Lennon', new Gender('U')),                 new Name('Sarah', new Gender('W'))             ],             $result         );     } } <\/code><\/pre>\n<p>  <a name=\"16\"><\/a><\/p>\n<h2>\u041d\u0430\u0431\u043b\u044e\u0434\u0430\u0435\u043c\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0438 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438<\/h2>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">final class ApplicationService {     public function __construct(private SubscriptionRepositoryInterface $subscriptionRepository) {}      public function renewSubscription(int $subscriptionId): bool     {         $subscription = $this-&gt;subscriptionRepository-&gt;findById($subscriptionId);          if (!$subscription-&gt;getStatus()-&gt;isEqual(Status::expired())) {             return false;         }          $subscription-&gt;setStatus(Status::active());         $subscription-&gt;setModifiedAt(new \\DateTimeImmutable());         return true;     } } final class Subscription {     private Status $status;      private \\DateTimeImmutable $modifiedAt;      public function __construct(Status $status, \\DateTimeImmutable $modifiedAt)     {         $this-&gt;status = $status;         $this-&gt;modifiedAt = $modifiedAt;     }      public function getStatus(): Status     {         return $this-&gt;status;     }      public function setStatus(Status $status): void     {         $this-&gt;status = $status;     }      public function getModifiedAt(): \\DateTimeImmutable     {         return $this-&gt;modifiedAt;     }      public function setModifiedAt(\\DateTimeImmutable $modifiedAt): void     {         $this-&gt;modifiedAt = $modifiedAt;     } } final class InvalidTestExample extends TestCase {     \/**      * @test      *\/     public function renew_an_expired_subscription_is_possible(): void     {         $modifiedAt = new \\DateTimeImmutable();         $expiredSubscription = new Subscription(Status::expired(), $modifiedAt);         $repository = $this-&gt;createStub(SubscriptionRepositoryInterface::class);         $repository-&gt;method('findById')-&gt;willReturn($expiredSubscription);         $sut = new ApplicationService($repository);          $result = $sut-&gt;renewSubscription(1);          self::assertSame(Status::active(), $expiredSubscription-&gt;getStatus());         self::assertGreaterThan($modifiedAt, $expiredSubscription-&gt;getModifiedAt());         self::assertTrue($result);     }      \/**      * @test      *\/     public function renew_an_active_subscription_is_not_possible(): void     {         $modifiedAt = new \\DateTimeImmutable();         $activeSubscription = new Subscription(Status::active(), $modifiedAt);         $repository = $this-&gt;createStub(SubscriptionRepositoryInterface::class);         $repository-&gt;method('findById')-&gt;willReturn($activeSubscription);         $sut = new ApplicationService($repository);          $result = $sut-&gt;renewSubscription(1);          self::assertSame($modifiedAt, $activeSubscription-&gt;getModifiedAt());         self::assertFalse($result);     } } <\/code><\/pre>\n<p>  \u0425\u043e\u0440\u043e\u0448\u043e:<\/p>\n<pre><code class=\"php\">final class ApplicationService {     public function __construct(private SubscriptionRepositoryInterface $subscriptionRepository) {}      public function renewSubscription(int $subscriptionId): bool     {         $subscription = $this-&gt;subscriptionRepository-&gt;findById($subscriptionId);         return $subscription-&gt;renew(new \\DateTimeImmutable());     } } final class Subscription {     private Status $status;      private \\DateTimeImmutable $modifiedAt;      public function __construct(\\DateTimeImmutable $modifiedAt)     {         $this-&gt;status = Status::new();         $this-&gt;modifiedAt = $modifiedAt;     }      public function renew(\\DateTimeImmutable $modifiedAt): bool     {         if (!$this-&gt;status-&gt;isEqual(Status::expired())) {             return false;         }          $this-&gt;status = Status::active();         $this-&gt;modifiedAt = $modifiedAt;         return true;     }      public function active(\\DateTimeImmutable $modifiedAt): void     {         \/\/simplified         $this-&gt;status = Status::active();         $this-&gt;modifiedAt = $modifiedAt;     }      public function expire(\\DateTimeImmutable $modifiedAt): void     {         \/\/simplified         $this-&gt;status = Status::expired();         $this-&gt;modifiedAt = $modifiedAt;     }      public function isActive(): bool     {         return $this-&gt;status-&gt;isEqual(Status::active());     } } final class ValidTestExample extends TestCase {     \/**      * @test      *\/     public function renew_an_expired_subscription_is_possible(): void     {         $expiredSubscription = SubscriptionMother::expired();         $repository = $this-&gt;createStub(SubscriptionRepositoryInterface::class);         $repository-&gt;method('findById')-&gt;willReturn($expiredSubscription);         $sut = new ApplicationService($repository);          $result = $sut-&gt;renewSubscription(1);          \/\/ skip checking modifiedAt as it's not a part of observable behavior. To check this value we         \/\/ would have to add a getter for modifiedAt, probably only for test purposes.         self::assertTrue($expiredSubscription-&gt;isActive());         self::assertTrue($result);     }      \/**      * @test      *\/     public function renew_an_active_subscription_is_not_possible(): void     {         $activeSubscription = SubscriptionMother::active();         $repository = $this-&gt;createStub(SubscriptionRepositoryInterface::class);         $repository-&gt;method('findById')-&gt;willReturn($activeSubscription);         $sut = new ApplicationService($repository);          $result = $sut-&gt;renewSubscription(1);          self::assertTrue($activeSubscription-&gt;isActive());         self::assertFalse($result);     } } <\/code><\/pre>\n<p>  \u0423 \u043f\u0435\u0440\u0432\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u043f\u043b\u043e\u0445\u0430\u044f \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430. \u0414\u043b\u044f \u0432\u044b\u0437\u043e\u0432\u0430 \u043e\u0434\u043d\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u0440\u0438 \u043c\u0435\u0442\u043e\u0434\u0430. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0435\u0442\u043e\u0434\u044b-\u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 (\u0433\u0435\u0442\u0442\u0435\u0440\u044b) \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438. \u0412 \u0434\u0430\u043d\u043d\u043e\u043c \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u043f\u0440\u043e\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f <code>modifiedAt<\/code>. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0435 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e <code>modifiedAt<\/code> \u0432 \u0445\u043e\u0434\u0435 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 <code>renew<\/code> \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0430\u043d\u0438\u044f. \u0414\u043b\u044f <code>modifiedAt<\/code> \u043c\u0435\u0442\u043e\u0434-\u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u044c \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f. \u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u0435\u0441\u0442\u044c \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u0438, \u0432 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043e\u0447\u0435\u043d\u044c \u0442\u0440\u0443\u0434\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0441\u043f\u043e\u0441\u043e\u0431 \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043c\u0435\u0442\u043e\u0434\u043e\u0432-\u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0435\u0439 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432, \u043d\u043e \u0438\u0445 \u043d\u0443\u0436\u043d\u043e \u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u0432\u0441\u0435\u043c\u0438 \u0441\u0438\u043b\u0430\u043c\u0438.<\/p>\n<p>  <a name=\"17\"><\/a><\/p>\n<h2>\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f<\/h2>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">class CannotSuspendExpiredSubscriptionPolicy implements SuspendingPolicyInterface {     public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool     {         if ($subscription-&gt;isExpired()) {             return false;         }          return true;     } } class CannotSuspendExpiredSubscriptionPolicyTest extends TestCase {     \/**      * @test      *\/     public function it_returns_true_when_a_subscription_is_expired(): void     {         $policy = new CannotSuspendExpiredSubscriptionPolicy();         $subscription = $this-&gt;createStub(Subscription::class);         $subscription-&gt;method('isExpired')-&gt;willReturn(true);          self::assertFalse($policy-&gt;suspend($subscription, new \\DateTimeImmutable()));     }      \/**      * @test      *\/     public function it_returns_false_when_a_subscription_is_not_expired(): void     {         $policy = new CannotSuspendExpiredSubscriptionPolicy();         $subscription = $this-&gt;createStub(Subscription::class);         $subscription-&gt;method('isExpired')-&gt;willReturn(false);          self::assertTrue($policy-&gt;suspend($subscription, new \\DateTimeImmutable()));     } } class CannotSuspendNewSubscriptionPolicy implements SuspendingPolicyInterface {     public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool     {         if ($subscription-&gt;isNew()) {             return false;         }          return true;     } } class CannotSuspendNewSubscriptionPolicyTest extends TestCase {     \/**      * @test      *\/     public function it_returns_false_when_a_subscription_is_new(): void     {         $policy = new CannotSuspendNewSubscriptionPolicy();         $subscription = $this-&gt;createStub(Subscription::class);         $subscription-&gt;method('isNew')-&gt;willReturn(true);          self::assertFalse($policy-&gt;suspend($subscription, new \\DateTimeImmutable()));     }      \/**      * @test      *\/     public function it_returns_true_when_a_subscription_is_not_new(): void     {         $policy = new CannotSuspendNewSubscriptionPolicy();         $subscription = $this-&gt;createStub(Subscription::class);         $subscription-&gt;method('isNew')-&gt;willReturn(false);          self::assertTrue($policy-&gt;suspend($subscription, new \\DateTimeImmutable()));     } } class CanSuspendAfterOneMonthPolicy implements SuspendingPolicyInterface {     public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool     {         $oneMonthEarlierDate = \\DateTime::createFromImmutable($at)-&gt;sub(new \\DateInterval('P1M'));          return $subscription-&gt;isOlderThan(\\DateTimeImmutable::createFromMutable($oneMonthEarlierDate));     } } class CanSuspendAfterOneMonthPolicyTest extends 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-&gt;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-&gt;suspend($subscription, $date));     } } class Status {     private const EXPIRED = 'expired';     private const ACTIVE = 'active';     private const NEW = 'new';     private const SUSPENDED = 'suspended';      private string $status;      private function __construct(string $status)     {         $this-&gt;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);     }      public static function suspended(): self     {         return new self(self::SUSPENDED);     }      public function isEqual(self $status): bool     {         return $this-&gt;status === $status-&gt;status;     } } class StatusTest extends TestCase {     public function testEquals(): void     {         $status1 = Status::active();         $status2 = Status::active();          self::assertTrue($status1-&gt;isEqual($status2));     }      public function testNotEquals(): void     {         $status1 = Status::active();         $status2 = Status::expired();          self::assertFalse($status1-&gt;isEqual($status2));     } } class SubscriptionTest extends TestCase {     \/**      * @test      *\/     public function suspending_a_subscription_is_possible_when_a_policy_returns_true(): void     {         $policy = $this-&gt;createMock(SuspendingPolicyInterface::class);         $policy-&gt;expects($this-&gt;once())-&gt;method('suspend')-&gt;willReturn(true);         $sut = new Subscription(new \\DateTimeImmutable());          $result = $sut-&gt;suspend($policy, new \\DateTimeImmutable());          self::assertTrue($result);         self::assertTrue($sut-&gt;isSuspended());     }      \/**      * @test      *\/     public function suspending_a_subscription_is_not_possible_when_a_policy_returns_false(): void     {         $policy = $this-&gt;createMock(SuspendingPolicyInterface::class);         $policy-&gt;expects($this-&gt;once())-&gt;method('suspend')-&gt;willReturn(false);         $sut = new Subscription(new \\DateTimeImmutable());          $result = $sut-&gt;suspend($policy, new \\DateTimeImmutable());          self::assertFalse($result);         self::assertFalse($sut-&gt;isSuspended());     }      \/**      * @test      *\/     public function it_returns_true_when_a_subscription_is_older_than_one_month(): void     {         $date = new \\DateTimeImmutable();         $futureDate = $date-&gt;add(new \\DateInterval('P1M'));         $sut = new Subscription($date);          self::assertTrue($sut-&gt;isOlderThan($futureDate));     }      \/**      * @test      *\/     public function it_returns_false_when_a_subscription_is_not_older_than_one_month(): void     {         $date = new \\DateTimeImmutable();         $futureDate = $date-&gt;add(new \\DateInterval('P1D'));         $sut = new Subscription($date);          self::assertTrue($sut-&gt;isOlderThan($futureDate));     } } <\/code><\/pre>\n<p>  <strong>\u041d\u0435 \u043f\u0438\u0448\u0438\u0442\u0435 \u043a\u043e\u0434 1:1: \u043e\u0434\u0438\u043d \u043a\u043b\u0430\u0441\u0441 \u2014 \u043e\u0434\u0438\u043d \u0442\u0435\u0441\u0442. \u042d\u0442\u043e \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u0445\u0440\u0443\u043f\u043a\u0438\u043c \u0442\u0435\u0441\u0442\u0430\u043c, \u0447\u0442\u043e \u0437\u0430\u0442\u0440\u0443\u0434\u043d\u044f\u0435\u0442 \u0440\u0435\u0444\u0430\u043a\u0442\u043e\u0440\u0438\u043d\u0433.<\/strong><\/p>\n<p>  \u0425\u043e\u0440\u043e\u0448\u043e:<\/p>\n<pre><code class=\"php\">final class CannotSuspendExpiredSubscriptionPolicy implements SuspendingPolicyInterface {     public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool     {         if ($subscription-&gt;isExpired()) {             return false;         }          return true;     } } final class CannotSuspendNewSubscriptionPolicy implements SuspendingPolicyInterface {     public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool     {         if ($subscription-&gt;isNew()) {             return false;         }          return true;     } } final class CanSuspendAfterOneMonthPolicy implements SuspendingPolicyInterface {     public function suspend(Subscription $subscription, \\DateTimeImmutable $at): bool     {         $oneMonthEarlierDate = \\DateTime::createFromImmutable($at)-&gt;sub(new \\DateInterval('P1M'));          return $subscription-&gt;isOlderThan(\\DateTimeImmutable::createFromMutable($oneMonthEarlierDate));     } } final class Status {     private const EXPIRED = 'expired';     private const ACTIVE = 'active';     private const NEW = 'new';     private const SUSPENDED = 'suspended';      private string $status;      private function __construct(string $status)     {         $this-&gt;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);     }      public static function suspended(): self     {         return new self(self::SUSPENDED);     }      public function isEqual(self $status): bool     {         return $this-&gt;status === $status-&gt;status;     } } final class Subscription {     private Status $status;      private \\DateTimeImmutable $createdAt;      public function __construct(\\DateTimeImmutable $createdAt)     {         $this-&gt;status = Status::new();         $this-&gt;createdAt = $createdAt;     }      public function suspend(SuspendingPolicyInterface $suspendingPolicy, \\DateTimeImmutable $at): bool     {         $result = $suspendingPolicy-&gt;suspend($this, $at);         if ($result) {             $this-&gt;status = Status::suspended();         }          return $result;     }      public function isOlderThan(\\DateTimeImmutable $date): bool     {         return $this-&gt;createdAt &lt; $date;     }      public function activate(): void     {         $this-&gt;status = Status::active();     }      public function expire(): void     {         $this-&gt;status = Status::expired();     }      public function isExpired(): bool     {         return $this-&gt;status-&gt;isEqual(Status::expired());     }      public function isActive(): bool     {         return $this-&gt;status-&gt;isEqual(Status::active());     }      public function isNew(): bool     {         return $this-&gt;status-&gt;isEqual(Status::new());     }      public function isSuspended(): bool     {         return $this-&gt;status-&gt;isEqual(Status::suspended());     } } final class SubscriptionSuspendingTest extends TestCase {     \/**      * @test      *\/     public function suspending_an_expired_subscription_with_cannot_suspend_expired_policy_is_not_possible(): void     {         $sut = new Subscription(new \\DateTimeImmutable());         $sut-&gt;activate();         $sut-&gt;expire();          $result = $sut-&gt;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-&gt;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-&gt;activate();          $result = $sut-&gt;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-&gt;activate();          $result = $sut-&gt;suspend(new CannotSuspendExpiredSubscriptionPolicy(), new \\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-&gt;suspend(new CanSuspendAfterOneMonthPolicy(), new \\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-&gt;suspend(new CanSuspendAfterOneMonthPolicy(), new \\DateTimeImmutable('2020-02-02'));          self::assertTrue($result);     } } <\/code><\/pre>\n<p>  <a name=\"18\"><\/a><\/p>\n<h2>\u0428\u0430\u0431\u043b\u043e\u043d humble<\/h2>\n<p>  \u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0430\u043a\u043e\u0433\u043e \u043a\u043b\u0430\u0441\u0441\u0430?<\/p>\n<pre><code class=\"php\">class ApplicationService {     public function __construct(         private OrderRepository $orderRepository,         private FormRepository $formRepository     ) {}      public function changeFormStatus(int $orderId): void     {         $order = $this-&gt;orderRepository-&gt;getById($orderId);         $soapResponse = $this-&gt;getSoapClient()-&gt;getStatusByOrderId($orderId);         $form = $this-&gt;formRepository-&gt;getByOrderId($orderId);         $form-&gt;setStatus($soapResponse['status']);         $form-&gt;setModifiedAt(new \\DateTimeImmutable());          if ($soapResponse['status'] === 'accepted') {             $order-&gt;setStatus('paid');         }          $this-&gt;formRepository-&gt;save($form);         $this-&gt;orderRepository-&gt;save($order);     }      private function getSoapClient(): \\SoapClient     {         return new \\SoapClient('https:\/\/legacy_system.pl\/Soap\/WebService', []);     } } <\/code><\/pre>\n<p>  \u041d\u0443\u0436\u043d\u043e \u0440\u0430\u0437\u0431\u0438\u0442\u044c \u0447\u0440\u0435\u0437\u043c\u0435\u0440\u043d\u043e \u0443\u0441\u043b\u043e\u0436\u043d\u0451\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u043d\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u043a\u043b\u0430\u0441\u0441\u044b.<\/p>\n<pre><code class=\"php\">final class ApplicationService {     public function __construct(         private OrderRepositoryInterface $orderRepository,         private FormRepositoryInterface $formRepository,         private FormApiInterface $formApi,         private ChangeFormStatusService $changeFormStatusService     ) {}      public function changeFormStatus(int $orderId): void     {         $order = $this-&gt;orderRepository-&gt;getById($orderId);         $form = $this-&gt;formRepository-&gt;getByOrderId($orderId);         $status = $this-&gt;formApi-&gt;getStatusByOrderId($orderId);          $this-&gt;changeFormStatusService-&gt;changeStatus($order, $form, $status);          $this-&gt;formRepository-&gt;save($form);         $this-&gt;orderRepository-&gt;save($order);     } } final class ChangeFormStatusService {     public function changeStatus(Order $order, Form $form, string $formStatus): void     {         $status = FormStatus::createFromString($formStatus);         $form-&gt;changeStatus($status);          if ($form-&gt;isAccepted()) {             $order-&gt;changeStatus(OrderStatus::paid());         }     } } final class ChangingFormStatusTest extends 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-&gt;changeStatus($order, $form, $status);          self::assertTrue($form-&gt;isAccepted());         self::assertTrue($order-&gt;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-&gt;changeStatus($order, $form, $status);          self::assertFalse($form-&gt;isAccepted());         self::assertFalse($order-&gt;isPaid());     } } <\/code><\/pre>\n<p>  \u041e\u0434\u043d\u0430\u043a\u043e <code>ApplicationService<\/code>, \u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e, \u043d\u0443\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0433\u043e \u0442\u0435\u0441\u0442\u0430 \u0441 \u043c\u043e\u043a\u043e\u043c <code>FormApiInterface<\/code>.<\/p>\n<p>  <a name=\"19\"><\/a><\/p>\n<h2>\u0411\u0435\u0441\u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0439 \u0442\u0435\u0441\u0442<\/h2>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">final class Customer {     public function __construct(private string $name) {}      public function getName(): string     {         return $this-&gt;name;     }      public function setName(string $name): void     {         $this-&gt;name = $name;     } } final class CustomerTest extends TestCase {     public function testSetName(): void     {         $customer = new Customer('Jack');          $customer-&gt;setName('John');          self::assertSame('John', $customer-&gt;getName());     } } final class EventSubscriber {     public static function getSubscribedEvents(): array     {         return ['event' =&gt; 'onEvent'];     }      public function onEvent(): void     {      } } final class EventSubscriberTest extends TestCase {     public function testGetSubscribedEvents(): void     {         $result = EventSubscriber::getSubscribedEvents();          self::assertSame(['event' =&gt; 'onEvent'], $result);     } } <\/code><\/pre>\n<p>  \u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u0434, \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u043a\u0430\u043a\u043e\u0439-\u043b\u0438\u0431\u043e \u0441\u043b\u043e\u0436\u043d\u043e\u0439 \u043b\u043e\u0433\u0438\u043a\u0438, \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0431\u0435\u0441\u0441\u043c\u044b\u0441\u043b\u0435\u043d\u043d\u043e, \u043d\u043e \u0438 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u0445\u0440\u0443\u043f\u043a\u0438\u043c \u0442\u0435\u0441\u0442\u0430\u043c.<\/p>\n<p>  <a name=\"20\"><\/a><\/p>\n<h2>\u0425\u0440\u0443\u043f\u043a\u0438\u0439 \u0442\u0435\u0441\u0442<\/h2>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">final class UserRepository {     public function __construct(         private Connection $connection     ) {}      public function getUserNameByEmail(string $email): ?array     {         return $this             -&gt;connection             -&gt;createQueryBuilder()             -&gt;from('user', 'u')             -&gt;where('u.email = :email')             -&gt;setParameter('email', $email)             -&gt;execute()             -&gt;fetch();     } } final class TestUserRepository extends TestCase {     public function testGetUserNameByEmail(): void     {         $email = 'test@test.com';         $connection = $this-&gt;createMock(Connection::class);         $queryBuilder = $this-&gt;createMock(QueryBuilder::class);         $result = $this-&gt;createMock(ResultStatement::class);         $userRepository = new UserRepository($connection);         $connection             -&gt;expects($this-&gt;once())             -&gt;method('createQueryBuilder')             -&gt;willReturn($queryBuilder);         $queryBuilder             -&gt;expects($this-&gt;once())             -&gt;method('from')             -&gt;with('user', 'u')             -&gt;willReturn($queryBuilder);         $queryBuilder             -&gt;expects($this-&gt;once())             -&gt;method('where')             -&gt;with('u.email = :email')             -&gt;willReturn($queryBuilder);         $queryBuilder             -&gt;expects($this-&gt;once())             -&gt;method('setParameter')             -&gt;with('email', $email)             -&gt;willReturn($queryBuilder);         $queryBuilder             -&gt;expects($this-&gt;once())             -&gt;method('execute')             -&gt;willReturn($result);         $result             -&gt;expects($this-&gt;once())             -&gt;method('fetch')             -&gt;willReturn(['email' =&gt; $email]);          $result = $userRepository-&gt;getUserNameByEmail($email);          self::assertSame(['email' =&gt; $email], $result);     } } <\/code><\/pre>\n<p>  \u041f\u043e\u0434\u043e\u0431\u043d\u043e\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0435\u0432 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u0445\u0440\u0443\u043f\u043a\u0438\u043c \u0442\u0435\u0441\u0442\u0430\u043c \u0438 \u0437\u0430\u0442\u0440\u0443\u0434\u043d\u044f\u0435\u0442 \u0440\u0435\u0444\u0430\u043a\u0442\u043e\u0440\u0438\u043d\u0433. \u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0442\u0435\u0441\u0442\u043e\u0432.<\/p>\n<p>  <a name=\"21\"><\/a><\/p>\n<h2>\u0418\u0441\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432<\/h2>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">final class InvalidTest extends TestCase {     private ?Subscription $subscription;      public function setUp(): void     {         $this-&gt;subscription = new Subscription(new \\DateTimeImmutable());         $this-&gt;subscription-&gt;activate();     }      \/**      * @test      *\/     public function suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void     {         $result = $this-&gt;subscription-&gt;suspend(new CannotSuspendNewSubscriptionPolicy(), new \\DateTimeImmutable());          self::assertTrue($result);     }      \/**      * @test      *\/     public function suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void     {         $result = $this-&gt;subscription-&gt;suspend(new CannotSuspendExpiredSubscriptionPolicy(), new \\DateTimeImmutable());          self::assertTrue($result);     }      \/**      * @test      *\/     public function suspending_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible(): void     {         \/\/ Here we need to create a new subscription, it is not possible to change $this-&gt;subscription to a new subscription     } } <\/code><\/pre>\n<p>  \u0425\u043e\u0440\u043e\u0448\u043e:<\/p>\n<pre><code class=\"php\">final class ValidTest extends TestCase {     \/**      * @test      *\/     public function suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void     {         $sut = $this-&gt;createAnActiveSubscription();          $result = $sut-&gt;suspend(new CannotSuspendNewSubscriptionPolicy(), new \\DateTimeImmutable());          self::assertTrue($result);     }      \/**      * @test      *\/     public function suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void     {         $sut = $this-&gt;createAnActiveSubscription();          $result = $sut-&gt;suspend(new CannotSuspendExpiredSubscriptionPolicy(), new \\DateTimeImmutable());          self::assertTrue($result);     }      \/**      * @test      *\/     public function suspending_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible(): void     {         $sut = $this-&gt;createANewSubscription();          $result = $sut-&gt;suspend(new CannotSuspendNewSubscriptionPolicy(), new \\DateTimeImmutable());          self::assertFalse($result);     }      private function createANewSubscription(): Subscription     {         return new Subscription(new \\DateTimeImmutable());     }      private function createAnActiveSubscription(): Subscription     {         $subscription = new Subscription(new \\DateTimeImmutable());         $subscription-&gt;activate();         return $subscription;     } } <\/code><\/pre>\n<ul>\n<li>\u041b\u0443\u0447\u0448\u0435 \u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0431\u0449\u0435\u0433\u043e \u0434\u043b\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0442\u0435\u0441\u0442\u043e\u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f.<\/li>\n<li>\u0427\u0442\u043e\u0431\u044b \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0432 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0442\u0435\u0441\u0442\u0430\u0445 \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0439\u0442\u0435:<\/li>\n<\/ul>\n<p>  <a name=\"22\"><\/a><\/p>\n<h2>\u041e\u0431\u0449\u0438\u0435 \u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f<\/h2>\n<p>  <a name=\"23\"><\/a><\/p>\n<h3>\u0420\u0430\u0441\u043a\u0440\u044b\u0442\u0438\u0435 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f<\/h3>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">final class Customer {     private CustomerType $type;      private DiscountCalculationPolicyInterface $discountCalculationPolicy;      public function __construct()     {         $this-&gt;type = CustomerType::NORMAL();         $this-&gt;discountCalculationPolicy = new NormalDiscountPolicy();     }      public function makeVip(): void     {         $this-&gt;type = CustomerType::VIP();         $this-&gt;discountCalculationPolicy = new VipDiscountPolicy();     }      public function getCustomerType(): CustomerType     {         return $this-&gt;type;     }      public function getPercentageDiscount(): int     {         return $this-&gt;discountCalculationPolicy-&gt;getPercentageDiscount();     } } final class InvalidTest extends TestCase {     public function testMakeVip(): void     {         $sut = new Customer();         $sut-&gt;makeVip();          self::assertSame(CustomerType::VIP(), $sut-&gt;getCustomerType());     } } <\/code><\/pre>\n<p>  \u0425\u043e\u0440\u043e\u0448\u043e:<\/p>\n<pre><code class=\"php\">final class Customer {     private CustomerType $type;      private DiscountCalculationPolicyInterface $discountCalculationPolicy;      public function __construct()     {         $this-&gt;type = CustomerType::NORMAL();         $this-&gt;discountCalculationPolicy = new NormalDiscountPolicy();     }      public function makeVip(): void     {         $this-&gt;type = CustomerType::VIP();         $this-&gt;discountCalculationPolicy = new VipDiscountPolicy();     }      public function getPercentageDiscount(): int     {         return $this-&gt;discountCalculationPolicy-&gt;getPercentageDiscount();     } } final class ValidTest extends TestCase {     \/**      * @test      *\/     public function a_vip_customer_has_a_25_percentage_discount(): void     {         $sut = new Customer();         $sut-&gt;makeVip();          self::assertSame(25, $sut-&gt;getPercentageDiscount());     } } <\/code><\/pre>\n<p>  \u0412\u043d\u0435\u0441\u0435\u043d\u0438\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e production-\u043a\u043e\u0434\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043c\u0435\u0442\u043e\u0434\u0430-\u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u044f <code>getCustomerType()<\/code>) \u0442\u043e\u043b\u044c\u043a\u043e \u0440\u0430\u0434\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0442\u0435\u0441\u0442\u0430\u0445 \u2014 \u043f\u043b\u043e\u0445\u0430\u044f \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0430. \u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043d\u0443\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0434\u0440\u0443\u0433\u0438\u043c \u0432\u0430\u0436\u043d\u044b\u043c \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c (\u0432 \u044d\u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u2014 <code>getPercentageDiscount()<\/code>). \u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u0438\u043d\u043e\u0433\u0434\u0430 \u0442\u0440\u0443\u0434\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438, \u0438 \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f \u0432\u044b\u043d\u0443\u0436\u0434\u0435\u043d\u044b \u0432\u043d\u0435\u0441\u0442\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 production-\u043a\u043e\u0434 \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0441\u0442\u0438 \u0442\u0435\u0441\u0442\u043e\u0432, \u043d\u043e \u043d\u0443\u0436\u043d\u043e \u0441\u0442\u0430\u0440\u0430\u0442\u044c\u0441\u044f \u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u044d\u0442\u043e\u0433\u043e.<\/p>\n<p>  <a name=\"24\"><\/a><\/p>\n<h3>\u0423\u0442\u0435\u0447\u043a\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0435\u0439 \u043e \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043d\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438<\/h3>\n<p>  <\/p>\n<pre><code class=\"php\">final class DiscountCalculator {     public function calculate(int $isVipFromYears): int     {         Assert::greaterThanEq($isVipFromYears, 0);         return min(($isVipFromYears * 10) + 3, 80);     } } <\/code><\/pre>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">final class InvalidTest extends TestCase {     \/**      * @dataProvider discountDataProvider      *\/     public function testCalculate(int $vipDaysFrom, int $expected): void     {         $sut = new DiscountCalculator();          self::assertSame($expected, $sut-&gt;calculate($vipDaysFrom));     }      public function discountDataProvider(): array     {         return [             [0, 0 * 10 + 3], \/\/leaking domain details             [1, 1 * 10 + 3],             [5, 5 * 10 + 3],             [8, 80]         ];     } } <\/code><\/pre>\n<p>  \u0425\u043e\u0440\u043e\u0448\u043e:<\/p>\n<pre><code class=\"php\">final class ValidTest extends TestCase {     \/**      * @dataProvider discountDataProvider      *\/     public function testCalculate(int $vipDaysFrom, int $expected): void     {         $sut = new DiscountCalculator();          self::assertSame($expected, $sut-&gt;calculate($vipDaysFrom));     }      public function discountDataProvider(): array     {         return [             [0, 3],             [1, 13],             [5, 53],             [8, 80]         ];     } } <\/code><\/pre>\n<p>  \u041d\u0435 \u0434\u0443\u0431\u043b\u0438\u0440\u0443\u0439\u0442\u0435 \u0432 \u0442\u0435\u0441\u0442\u0430\u0445 production-\u043b\u043e\u0433\u0438\u043a\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0439\u0442\u0435 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0445 \u0432 \u043a\u043e\u0434\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439.<\/p>\n<p>  <a name=\"25\"><\/a><\/p>\n<h3>\u041c\u043e\u043a\u0438\u043d\u0433 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u043a\u043b\u0430\u0441\u0441\u043e\u0432<\/h3>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">class DiscountCalculator {     public function calculateInternalDiscount(int $isVipFromYears): int     {         Assert::greaterThanEq($isVipFromYears, 0);         return min(($isVipFromYears * 10) + 3, 80);     }      public function calculateAdditionalDiscountFromExternalSystem(): int     {         \/\/ get data from an external system to calculate a discount         return 5;     } } class OrderService {     public function __construct(private DiscountCalculator $discountCalculator) {}      public function getTotalPriceWithDiscount(int $totalPrice, int $vipFromDays): int     {         $internalDiscount = $this-&gt;discountCalculator-&gt;calculateInternalDiscount($vipFromDays);         $externalDiscount = $this-&gt;discountCalculator-&gt;calculateAdditionalDiscountFromExternalSystem();         $discountSum = $internalDiscount + $externalDiscount;         return $totalPrice - (int) ceil(($totalPrice * $discountSum) \/ 100);     } } final class InvalidTest extends TestCase {     \/**      * @dataProvider orderDataProvider      *\/     public function testGetTotalPriceWithDiscount(int $totalPrice, int $vipDaysFrom, int $expected): void     {         $discountCalculator = $this-&gt;createPartialMock(DiscountCalculator::class, ['calculateAdditionalDiscountFromExternalSystem']);         $discountCalculator-&gt;method('calculateAdditionalDiscountFromExternalSystem')-&gt;willReturn(5);         $sut = new OrderService($discountCalculator);          self::assertSame($expected, $sut-&gt;getTotalPriceWithDiscount($totalPrice, $vipDaysFrom));     }      public function orderDataProvider(): array     {         return [             [1000, 0, 920],             [500, 1, 410],             [644, 5, 270],         ];     } } <\/code><\/pre>\n<p>  \u0425\u043e\u0440\u043e\u0448\u043e:<\/p>\n<pre><code class=\"php\">interface ExternalDiscountCalculatorInterface {     public function calculate(): int; } final class InternalDiscountCalculator {     public function calculate(int $isVipFromYears): int     {         Assert::greaterThanEq($isVipFromYears, 0);         return min(($isVipFromYears * 10) + 3, 80);     } } final class OrderService {     public function __construct(         private InternalDiscountCalculator $discountCalculator,         private ExternalDiscountCalculatorInterface $externalDiscountCalculator     ) {}      public function getTotalPriceWithDiscount(int $totalPrice, int $vipFromDays): int     {         $internalDiscount = $this-&gt;discountCalculator-&gt;calculate($vipFromDays);         $externalDiscount = $this-&gt;externalDiscountCalculator-&gt;calculate();         $discountSum = $internalDiscount + $externalDiscount;         return $totalPrice - (int) ceil(($totalPrice * $discountSum) \/ 100);     } } final class ValidTest extends TestCase {     \/**      * @dataProvider orderDataProvider      *\/     public function testGetTotalPriceWithDiscount(int $totalPrice, int $vipDaysFrom, int $expected): void     {         $externalDiscountCalculator = $this-&gt;createStub(ExternalDiscountCalculatorInterface::class);         $externalDiscountCalculator-&gt;method('calculate')-&gt;willReturn(5);         $sut = new OrderService(new InternalDiscountCalculator(), $externalDiscountCalculator);          self::assertSame($expected, $sut-&gt;getTotalPriceWithDiscount($totalPrice, $vipDaysFrom));     }      public function orderDataProvider(): array     {         return [             [1000, 0, 920],             [500, 1, 410],             [644, 5, 270],         ];     } } <\/code><\/pre>\n<p>  \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u044c \u043c\u043e\u043a\u0430\u0442\u044c \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043b\u0430\u0441\u0441 \u0434\u043b\u044f \u0437\u0430\u043c\u0435\u043d\u044b \u0447\u0430\u0441\u0442\u0438 \u0435\u0433\u043e \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u043a\u043b\u0430\u0441\u0441, \u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e, \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0441\u043b\u043e\u0436\u0435\u043d \u0438 \u043d\u0430\u0440\u0443\u0448\u0430\u0435\u0442 \u043f\u0440\u0438\u043d\u0446\u0438\u043f \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0439 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438.<\/p>\n<p>  <a name=\"26\"><\/a><\/p>\n<h3>\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432<\/h3>\n<p>  <\/p>\n<pre><code class=\"php\">final class OrderItem {     public function __construct(private int $total) {}      public function getTotal(): int     {         return $this-&gt;total;     } } final class Order {     \/**      * @param OrderItem[] $items      * @param int $transportCost      *\/     public function __construct(private array $items, private int $transportCost) {}      public function getTotal(): int     {         return $this-&gt;getItemsTotal() + $this-&gt;transportCost;     }      private function getItemsTotal(): int     {         return array_reduce(             array_map(fn (OrderItem $item) =&gt; $item-&gt;getTotal(), $this-&gt;items),             fn (int $sum, int $total) =&gt; $sum += $total,             0         );     } } <\/code><\/pre>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">final class InvalidTest extends TestCase {     \/**      * @test      * @dataProvider ordersDataProvider      *\/     public function get_total_returns_a_total_cost_of_a_whole_order(Order $order, int $expectedTotal): void     {         self::assertSame($expectedTotal, $order-&gt;getTotal());     }      \/**      * @test      * @dataProvider orderItemsDataProvider      *\/     public function get_items_total_returns_a_total_cost_of_all_items(Order $order, int $expectedTotal): void     {         self::assertSame($expectedTotal, $this-&gt;invokePrivateMethodGetItemsTotal($order));     }      public function ordersDataProvider(): array     {         return [             [new Order([new OrderItem(20), new OrderItem(20), new OrderItem(20)], 15), 75],             [new Order([new OrderItem(20), new OrderItem(30), new OrderItem(40)], 0), 90],             [new Order([new OrderItem(99), new OrderItem(99), new OrderItem(99)], 9), 306]         ];     }      public function orderItemsDataProvider(): array     {         return [             [new Order([new OrderItem(20), new OrderItem(20), new OrderItem(20)], 15), 60],             [new Order([new OrderItem(20), new OrderItem(30), new OrderItem(40)], 0), 90],             [new Order([new OrderItem(99), new OrderItem(99), new OrderItem(99)], 9), 297]         ];     }      private function invokePrivateMethodGetItemsTotal(Order &amp;$order): int     {         $reflection = new \\ReflectionClass(get_class($order));         $method = $reflection-&gt;getMethod('getItemsTotal');         $method-&gt;setAccessible(true);         return $method-&gt;invokeArgs($order, []);     } } <\/code><\/pre>\n<p>  \u0425\u043e\u0440\u043e\u0448\u043e:<\/p>\n<pre><code class=\"php\">final class ValidTest extends TestCase {     \/**      * @test      * @dataProvider ordersDataProvider      *\/     public function get_total_returns_a_total_cost_of_a_whole_order(Order $order, int $expectedTotal): void     {         self::assertSame($expectedTotal, $order-&gt;getTotal());     }      public function ordersDataProvider(): array     {         return [             [new Order([new OrderItem(20), new OrderItem(20), new OrderItem(20)], 15), 75],             [new Order([new OrderItem(20), new OrderItem(30), new OrderItem(40)], 0), 90],             [new Order([new OrderItem(99), new OrderItem(99), new OrderItem(99)], 9), 306]         ];     } } <\/code><\/pre>\n<p>  \u0422\u0435\u0441\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 API.<\/p>\n<p>  <a name=\"27\"><\/a><\/p>\n<h3>\u0412\u0440\u0435\u043c\u044f \u043a\u0430\u043a \u043d\u0435\u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u0430\u044f \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c<\/h3>\n<p>  \u0412\u0440\u0435\u043c\u044f \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0435\u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0439 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c\u044e \u0438\u0437-\u0437\u0430 \u0441\u0432\u043e\u0435\u0433\u043e \u043d\u0435\u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0437\u043c\u0430. \u041a\u0430\u0436\u0434\u044b\u0439 \u0432\u044b\u0437\u043e\u0432 \u0434\u0430\u0451\u0442 \u0434\u0440\u0443\u0433\u043e\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442.<\/p>\n<p>  \u041f\u043b\u043e\u0445\u043e:<\/p>\n<pre><code class=\"php\">final class Clock {     public static \\DateTime|null $currentDateTime = null;      public static function getCurrentDateTime(): \\DateTime     {         if (null === self::$currentDateTime) {             self::$currentDateTime = new \\DateTime();         }          return self::$currentDateTime;     }      public static function set(\\DateTime $dateTime): void     {         self::$currentDateTime = $dateTime;     }      public static function reset(): void     {         self::$currentDateTime = null;     } } final class Customer {     private \\DateTime $createdAt;      public function __construct()     {         $this-&gt;createdAt = Clock::getCurrentDateTime();     }      public function isVip(): bool     {         return $this-&gt;createdAt-&gt;diff(Clock::getCurrentDateTime())-&gt;y &gt;= 1;     } } final class InvalidTest extends TestCase {     \/**      * @test      *\/     public function a_customer_registered_more_than_a_one_year_ago_is_a_vip(): void     {         Clock::set(new \\DateTime('2019-01-01'));         $sut = new Customer();         Clock::reset(); \/\/ you have to remember about resetting the shared state          self::assertTrue($sut-&gt;isVip());     }      \/**      * @test      *\/     public function a_customer_registered_less_than_a_one_year_ago_is_not_a_vip(): void     {         Clock::set((new \\DateTime())-&gt;sub(new \\DateInterval('P2M')));         $sut = new Customer();         Clock::reset(); \/\/ you have to remember about resetting the shared state          self::assertFalse($sut-&gt;isVip());     } } <\/code><\/pre>\n<p>  \u0425\u043e\u0440\u043e\u0448\u043e:<\/p>\n<pre><code class=\"php\">interface ClockInterface {     public function getCurrentTime(): \\DateTimeImmutable; } final class Clock implements ClockInterface {     private function __construct()     {     }      public static function create(): self     {         return new self();     }      public function getCurrentTime(): \\DateTimeImmutable     {         return new \\DateTimeImmutable();     } } final class FixedClock implements ClockInterface {     private function __construct(private \\DateTimeImmutable $fixedDate) {}      public static function create(\\DateTimeImmutable $fixedDate): self     {         return new self($fixedDate);     }      public function getCurrentTime(): \\DateTimeImmutable     {         return $this-&gt;fixedDate;     } } final class Customer {     private \\DateTimeImmutable $createdAt;      public function __construct(\\DateTimeImmutable $createdAt)     {         $this-&gt;createdAt = $createdAt;     }      public function isVip(\\DateTimeImmutable $currentDate): bool     {         return $this-&gt;createdAt-&gt;diff($currentDate)-&gt;y &gt;= 1;     } } final class ValidTest extends TestCase {     \/**      * @test      *\/     public function a_customer_registered_more_than_a_one_year_ago_is_a_vip(): void     {         $sut = new Customer(FixedClock::create(new \\DateTimeImmutable('2019-01-01'))-&gt;getCurrentTime());          self::assertTrue($sut-&gt;isVip(FixedClock::create(new \\DateTimeImmutable('2020-01-02'))-&gt;getCurrentTime()));     }      \/**      * @test      *\/     public function a_customer_registered_less_than_a_one_year_ago_is_not_a_vip(): void     {         $sut = new Customer(FixedClock::create(new \\DateTimeImmutable('2019-01-01'))-&gt;getCurrentTime());          self::assertFalse($sut-&gt;isVip(FixedClock::create(new \\DateTimeImmutable('2019-05-02'))-&gt;getCurrentTime()));     } } <\/code><\/pre>\n<p>  \u0412 \u043a\u043e\u0434\u0435, \u043e\u0442\u043d\u043e\u0441\u044f\u0449\u0435\u043c\u0441\u044f \u043a \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043d\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438, \u043d\u0435\u043b\u044c\u0437\u044f \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u044f \u0438 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0435 \u0447\u0438\u0441\u043b\u0430. \u0414\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u043d\u0443\u0436\u043d\u044b \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0441\u0442\u0441\u043a\u0438\u0435 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0443\u0436\u043d\u043e \u0432\u043d\u0435\u0434\u0440\u044f\u0442\u044c \u044d\u0442\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432 \u043e\u0431\u044a\u0435\u043a\u0442, \u043e\u0442\u043d\u043e\u0441\u044f\u0449\u0438\u0439\u0441\u044f \u043a \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043d\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438, \u043a\u0430\u043a \u0432 \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u0432\u044b\u0448\u0435.<\/p>\n<p>  <a name=\"28\"><\/a><\/p>\n<h2>\u041d\u0435 \u0433\u043e\u043d\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043b\u043d\u044b\u043c \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435\u043c<\/h2>\n<p>  \u041f\u043e\u043b\u043d\u043e\u0435 \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0446\u0435\u043b\u044c\u044e, \u0438\u043b\u0438 \u0434\u0430\u0436\u0435 \u043d\u0435 \u0436\u0435\u043b\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0432 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0442\u0435\u0441\u0442\u044b \u043d\u0430\u0432\u0435\u0440\u043d\u044f\u043a\u0430 \u0431\u0443\u0434\u0443\u0442 \u043e\u0447\u0435\u043d\u044c \u0445\u0440\u0443\u043f\u043a\u0438\u043c\u0438, \u0430 \u0440\u0435\u0444\u0430\u043a\u0442\u043e\u0440\u0438\u043d\u0433 \u2014 \u043e\u0447\u0435\u043d\u044c \u0441\u043b\u043e\u0436\u043d\u044b\u043c. \u041c\u0443\u0442\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0430\u0451\u0442 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u043b\u0435\u0437\u043d\u0443\u044e \u043e\u0431\u0440\u0430\u0442\u043d\u0443\u044e \u0441\u0432\u044f\u0437\u044c \u043e \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0442\u0435\u0441\u0442\u043e\u0432. <a href=\"https:\/\/sarvendev.com\/en\/2019\/06\/mutation-testing-we-are-testing-tests\/\">\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435<\/a>.<\/p>\n<p>  <a name=\"29\"><\/a><\/p>\n<h2>\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c\u044b\u0435 \u043a\u043d\u0438\u0433\u0438<\/h2>\n<p>  <\/p>\n<ul>\n<li><a href=\"https:\/\/www.amazon.com\/gp\/product\/0321146530\/\">Test Driven Development: By Example \/ Kent Beck<\/a> \u2014 \u043a\u043b\u0430\u0441\u0441\u0438\u043a\u0430.<\/li>\n<li><a href=\"https:\/\/www.amazon.com\/Unit-Testing-Principles-Practices-Patterns\/dp\/1617296279\">Unit Testing Principles, Practices, and Patterns \/ Vladimir Khorikov<\/a> \u2014 \u043b\u0443\u0447\u0448\u0430\u044f \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043c\u043d\u0435 \u043a\u043d\u0438\u0433\u0430 \u043e \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438.<\/li>\n<\/ul>\n<\/div>\n<p> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/company\/mailru\/blog\/549698\/\"> https:\/\/habr.com\/ru\/company\/mailru\/blog\/549698\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"\n<div class=\"post__text post__text-html post__text_v1\" id=\"post-content-body\">\n<div style=\"text-align:center;\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/1-\/oj\/3d\/1-oj3d2j1lkbnzrb2hbxsr5zreo.jpeg\"><\/div>\n<p>  \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u044b \u043d\u0435\u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u044b. \u0414\u0443\u043c\u0430\u044e, \u043e\u043d\u0438 \u0435\u0441\u0442\u044c \u0432 \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u0435 \u0438\u0437 \u043d\u0435\u0434\u0430\u0432\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432. \u042e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u044b \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0432\u0430\u0436\u043d\u0435\u0439\u0448\u0438\u043c\u0438 \u0432 enterprise-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f\u0445 \u0441 \u043e\u0431\u0438\u043b\u0438\u0435\u043c \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0438, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043e\u043d\u0438 \u0431\u044b\u0441\u0442\u0440\u044b\u0435 \u0438 \u043c\u043e\u0433\u0443\u0442 \u0441\u0440\u0430\u0437\u0443 \u0441\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u043c, \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u0430 \u043b\u0438 \u043d\u0430\u0448\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f. \u041e\u0434\u043d\u0430\u043a\u043e \u044f \u0447\u0430\u0441\u0442\u043e \u0441\u0442\u0430\u043b\u043a\u0438\u0432\u0430\u044e\u0441\u044c \u0441 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u043c\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u044b \u0441 \u0445\u043e\u0440\u043e\u0448\u0438\u043c\u0438 \u0442\u0435\u0441\u0442\u0430\u043c\u0438, \u0445\u043e\u0442\u044f \u0442\u0435 \u0438 \u043a\u0440\u0430\u0439\u043d\u0435 \u043f\u043e\u043b\u0435\u0437\u043d\u044b. \u042f \u0434\u0430\u043c \u0432\u0430\u043c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u043e\u0432\u0435\u0442\u043e\u0432 \u0441 \u043f\u0440\u0438\u043c\u0435\u0440\u0430\u043c\u0438, \u043a\u0430\u043a \u043f\u0438\u0441\u0430\u0442\u044c \u0445\u043e\u0440\u043e\u0448\u0438\u0435 \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u044b.  <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-320522","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/320522","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=320522"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/320522\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=320522"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=320522"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=320522"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}