في هذه الأوقات، فوائد اختبارات وحدة الكتابة هائلة. أعتقد أن معظم المشاريع التي بدأت مؤخرًا تحتوي على أي اختبارات للوحدات. في تطبيقات المؤسسات التي تحتوي على الكثير من منطق الأعمال، تعد اختبارات الوحدة هي الاختبارات الأكثر أهمية، لأنها سريعة ويمكننا التأكد على الفور من صحة تنفيذنا. ومع ذلك، غالبًا ما أرى مشكلة في الاختبارات الجيدة في المشاريع، على الرغم من أن فوائد هذه الاختبارات تكون ضخمة فقط عندما يكون لديك اختبارات وحدة جيدة. لذا، في هذه الأمثلة، سأحاول مشاركة بعض النصائح حول ما يجب فعله لكتابة اختبارات وحدة جيدة.
نسخة سهلة القراءة: https://testing-tips.sarvendev.com/
كميل روكزينسكي
المدونة: https://sarvendev.com/
ينكدين: https://www.linkedin.com/in/kamilruczynski/
دعمكم يعني العالم بالنسبة لي! إذا كنت قد استمتعت بهذا الدليل ووجدت قيمة في المعرفة المشتركة، فكر في دعمي على BuyMeCoffee:
أو ببساطة ترك نجمة في المستودع ومتابعتي على Twitter وGithub لتكون على اطلاع بجميع التحديثات. كرمك يغذي شغفي لإنشاء محتوى أكثر ثاقبة لك.
إذا كان لديك أي أفكار تحسين أو موضوع للكتابة عنه، فلا تتردد في إعداد طلب سحب أو أخبرني بذلك.
اشترك وأتقن اختبار الوحدة باستخدام كتابي الإلكتروني المجاني!
تفاصيل
لا يزال لدي قائمة طويلة جدًا من التحسينات التي يجب إدخالها على هذا الدليل حول اختبار الوحدة وسوف أقدمها في المستقبل القريب.
مقدمة
مؤلف
اختبار مزدوج
تسمية
نمط AAA
أم الكائن
منشئ
تأكيد الكائن
اختبار المعلمة
مدرستان لاختبار الوحدة
الكلاسيكية
ساخر
التبعيات
وهمية مقابل كعب
ثلاثة أنماط لاختبار الوحدة
الإخراج
ولاية
تواصل
البنية الوظيفية والاختبارات
السلوك الملحوظ مقابل تفاصيل التنفيذ
وحدة السلوك
نمط متواضع
اختبار تافه
اختبار هش
تركيبات الاختبار
اختبار عام للأنماط المضادة
فضح الدولة الخاصة
تسرب تفاصيل المجال
السخرية من الطبقات الخرسانية
اختبار الطرق الخاصة
الوقت باعتباره تبعية متقلبة
لا ينبغي أن تكون تغطية الاختبار بنسبة 100% هي الهدف
الكتب الموصى بها
اختبار الزوجي هو تبعيات وهمية تستخدم في الاختبارات.
الدمية هي مجرد تطبيق بسيط لا يفعل شيئًا.
تطبق الطبقة النهائية Mailer MailerInterface {إرسال وظيفة عامة (رسالة $message): باطلة { } }
المزيف هو تطبيق مبسط لمحاكاة السلوك الأصلي.
الطبقة النهائية InMemoryCustomerRepository تنفذ CustomerRepositoryInterface {/** * @var Customer[] */مصفوفة خاصة $customers;وظيفة عامة __construct() {$this->customers = []; }مخزن الوظائف العامة (العميل $customer): void{$this->customers[(string) $customer->id()->id()] = $customer; }وظيفة عامة get(CustomerId $id): Customer{if (!isset($this->customers[(string) $id->id()])) {throw new CustomerNotFoundException(); }return $this->customers[(string) $id->id()]; } الوظيفة العامة findByEmail(Email $email): Customer{foreach ($this->customers as $customer) {if ($customer->getEmail()->isEqual($email)) {return $customer; } }طرح CustomerNotFoundException(); } }
كعب الروتين هو أبسط تطبيق بسلوك مضمن.
الطبقة النهائية UniqueEmailSpecificationStub تنفذ UniqueEmailSpecificationInterface {وظيفة عامة هيUnique(البريد الإلكتروني $email): bool{return true; } }
$مواصفاتStub = $this->createStub(UniqueEmailSpecificationInterface::class);$specationStub->method('isUnique')->willReturn(true);
الجاسوس هو تطبيق للتحقق من سلوك معين.
تطبق الطبقة النهائية Mailer MailerInterface {/** * @var message[] */private array $messages; الوظيفة العامة __construct() {$this->messages = []; }إرسال الوظيفة العامة (رسالة $message): void{$this->messages[] = $message; }الوظيفة العامة getCountOfSentMessages(): int{return count($this->messages); } }
الوهمية عبارة عن تقليد تم تكوينه للتحقق من المكالمات على أحد المتعاونين.
$message = رسالة جديدة('[email protected]', 'Test', 'اختبار اختبار اختبار');$mailer = $this->createMock(MailerInterface::class);$mailer->expect($this-> مرة واحدة()) ->الطريقة('إرسال') ->with($this->equalTo($message));
[!تنبيه] للتحقق من التفاعلات الواردة، استخدم كعب روتين، ولكن للتحقق من التفاعلات الواردة، استخدم نموذجًا وهميًا.
المزيد: وهمية مقابل كعب
[!تحذير|النمط:مسطح|التسمية:ليس جيدًا]
يمتد TestExample من الفئة النهائية إلى TestCase {/** * @test */public function sends_all_notifications(): void{$message1 = new message();$message2 = new message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->method('getAll')->willReturn([$message1, $message2]);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('send') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|النمط:مسطح|التسمية:أفضل]
مقاومة أفضل لإعادة البناء
استخدام Refactor->Rename في طريقة معينة لا يؤدي إلى كسر الاختبار
سهولة القراءة
انخفاض تكلفة الصيانة
ليس من الضروري تعلم تلك الأطر المحاكاة المتطورة
مجرد كود PHP عادي بسيط
يمتد TestExample من الفئة النهائية إلى TestCase {/** * @test */public function sends_all_notifications(): void{$message1 = new message();$message2 = new message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->حفظ($message2);$mailer = new SpyMailer();$sut = new NotificationService($mailer, $messageRepository);$sut->send(); $mailer->assertThatMessagesHaveBeenSent([$message1, $message2]); } }
[!تحذير|النمط:مسطح|التسمية:ليس جيدًا]
اختبار الوظيفة العامة (): void{$subscription = SubscriptionMother::new();$subscription->activate();self::assertSame(Status::activated(), $subscription->status()); }
[!TIP|style:flat|label:حدد بوضوح ما تختبره]
الوظيفة العامة sut(): void{// sut = النظام قيد الاختبار$sut = SubscriptionMother::new();$sut->activate();self::assertSame(Status::activated(), $sut->status ()); }
[!تحذير|النمط:مسطح|التسمية:ليس جيدًا]
الوظيفة العامة it_throws_invalid_credentials_exception_when_sign_in_with_invalid_credentials(): void{ }الوظيفة العامة testCreatingWithATooShortPasswordIsNotPossible(): void{ }وظيفة عامة testDeactivateASubscription(): void{ }
[!TIP|النمط:مسطح|التسمية:أفضل]
يؤدي استخدام الشرطة السفلية إلى تحسين إمكانية القراءة
يجب أن يصف الاسم السلوك وليس التنفيذ
استخدم الأسماء بدون كلمات رئيسية تقنية. يجب أن تكون قابلة للقراءة لشخص غير مبرمج.
الوظيفة العامةsign_in_with_invalid_credentials_is_not_possible(): void{ }وظيفة عامة create_with_a_too_short_password_is_not_possible(): void{ }إلغاء تنشيط الوظيفة العامة_an_activated_subscription_is_valid(): void{ }إلغاء تنشيط الوظيفة العامة_an_inactive_subscription_is_invalid(): void{ }
ملحوظة
وصف السلوك مهم في اختبار سيناريوهات المجال. إذا كان الكود الخاص بك مجرد أداة مساعدة، فهو أقل أهمية.
لماذا قد يكون من المفيد لغير المبرمج قراءة اختبارات الوحدة؟
إذا كان هناك مشروع ذو منطق مجال معقد، فيجب أن يكون هذا المنطق واضحًا جدًا للجميع، لذلك تصف الاختبارات تفاصيل المجال بدون كلمات رئيسية تقنية، ويمكنك التحدث مع شركة ما بلغة مثل هذه الاختبارات. يجب أن تكون جميع التعليمات البرمجية المتعلقة بالمجال خالية من التفاصيل الفنية. لن يتمكن غير المبرمج من قراءة هذه الاختبارات. إذا كنت تريد التحدث عن النطاق، فستكون هذه الاختبارات مفيدة لمعرفة ما يفعله هذا النطاق. سيكون هناك وصف بدون تفاصيل فنية، على سبيل المثال، إرجاع قيمة فارغة، أو طرح استثناء، وما إلى ذلك. هذا النوع من المعلومات ليس له علاقة بالمجال، لذا لا ينبغي لنا استخدام هذه الكلمات الرئيسية.
ومن الشائع أيضًا تقديم، متى، ثم.
فصل ثلاثة أقسام من الاختبار:
الترتيب : إخضاع النظام للاختبار في الحالة المطلوبة. قم بإعداد التبعيات والوسائط وأخيرًا قم ببناء SUT.
الفعل : استدعاء عنصر تم اختباره.
التأكيد : التحقق من النتيجة أو الحالة النهائية أو التواصل مع المتعاونين.
[!TIP|النمط:مسطح|التسمية:جيد]
الوظيفة العامة aaa_pattern_example_test(): void{//Arrange|Given$sut = SubscriptionMother::new();//Act|When$sut->activate();//Assert|Thenself::assertSame(Status::activated( ), $sut->status()); }
يساعد النمط على إنشاء كائنات محددة يمكن إعادة استخدامها في بعض الاختبارات. وبسبب ذلك فإن قسم الترتيب موجز والاختبار ككل أكثر قابلية للقراءة.
اشتراك الطبقة النهائيةMother {وظيفة ثابتة عامة جديدة (): الاشتراك {إرجاع الاشتراك الجديد () ؛ } تم تنشيط الوظيفة الثابتة العامة (): الاشتراك {$subscription = new Subscription();$subscription->activate();return $subscription; } تم إلغاء تنشيط الوظيفة الثابتة العامة (): الاشتراك {$subscription = self::activated();$subscription->deactivate();return $subscription; } }
اختبار الطبقة النهائية {وظيفة عامة example_test_with_activated_subscription(): void{$activatedSubscription = SubscriptionMother::activated();// افعل شيئًا// تحقق من شيء}وظيفة عامة example_test_with_deactivated_subscription(): void{$deactivatedSubscription = SubscriptionMother::deactivated();// افعل شيئًا // تحقق من شيء ما} }
Builder هو نمط آخر يساعدنا في إنشاء كائنات في الاختبارات. مقارنةً بـ Object Mother Pattern Builder، فهو أفضل لإنشاء كائنات أكثر تعقيدًا.
الطبقة النهائية OrderBuilder {private DateTimeImmutable|null $createdAt = null;/** * @var OrderItem[] */private array $items = [];وظيفة عامة createAt(DateTimeImmutable $createdAt): self{$this->createdAt = $createdAt;return $هذا; }وظيفة عامة withItem(string $name, int $price): self{$this->items[] = new OrderItem($name, $price);return $this; }بناء الوظيفة العامة (): الطلب{ Assert::notEmpty($this->items);return new Order($this->createdAt ?? new DateTimeImmutable(),$this->items, ); } }
يمتد ExampalTest من الفئة النهائية إلى TestCase {/** * @test */الوظيفة العامة example_test_with_order_builder(): void{$order = (new OrderBuilder()) ->createdAt(new DateTimeImmutable('2022-11-10 20:00:00')) ->مع العنصر('العنصر 1'، 1000) ->مع العنصر('البند 2'، 2000) ->مع العنصر('العنصر 3'، 3000) ->build();// افعل شيئًا// تحقق من شيء ما} }
يساعد نمط كائن التأكيد على كتابة أقسام تأكيد أكثر قابلية للقراءة. بدلاً من استخدام بعض التأكيدات، يمكننا فقط إعداد تجريد واستخدام اللغة الطبيعية لوصف النتيجة المتوقعة.
يمتد ExampalTest من الفئة النهائية إلى TestCase {/** * @test */public function example_test_with_asserter(): void{$currentTime = new DateTimeImmutable('2022-11-10 20:00:00');$sut = new OrderService();$order = $sut ->إنشاء($currentTime); OrderAsserter::assertThat($order) ->تم الإنشاء في($currentTime) ->hasTotal(6000); } }
استخدم PHPUnitFrameworkAssert;الطبقة النهائية OrderAsserter {وظيفة عامة __construct(خاص للقراءة فقط Order $order) {}وظيفة ثابتة عامةassertThat(Order $order): self{return new OrderAsserter($order); }الوظيفة العامة كانتCreatedAt(DateTimeImmutable $createdAt): self{ Assert::assertEquals($createdAt, $this->order->createdAt);return $this; }الوظيفة العامة hasTotal(int $total): self{ Assert::assertSame($total, $this->order->getTotal());return $this; } }
يعد الاختبار ذو المعلمات خيارًا جيدًا لاختبار SUT مع العديد من المعلمات دون تكرار الكود.
تحذير
هذا النوع من الاختبار أقل قابلية للقراءة. لزيادة سهولة القراءة قليلاً، يجب تقسيم الأمثلة السلبية والإيجابية إلى اختبارات مختلفة.
يمتد ExampalTest من الفئة النهائية إلى TestCase {/** * @test * @dataProvider getInvalidEmails */ الوظيفة العامة Detects_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( نتيجة $); }/** * @test * @dataProvider getValidEmails */ الوظيفة العامة Detects_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( نتيجة $); }الوظيفة العامة getInvalidEmails(): iterable{yield 'بريد إلكتروني غير صالح بدون @' => ['test'];yield 'بريد إلكتروني غير صالح بدون المجال بعد @' => ['test@'];yield 'بريد إلكتروني غير صالح بدون TLD' => ['test@test'];//...} الوظيفة العامة getValidEmails(): iterable{yield "بريد إلكتروني صالح بأحرف صغيرة" => ['[email protected]'];yield 'بريد إلكتروني صالح بأحرف وأرقام صغيرة' => ['[email protected]'];yield 'بريد إلكتروني صالح بأحرف كبيرة وأرقام' => ['Test123@'] test.com'];//...} }
ملحوظة
استخدم yield
وأضف وصفًا نصيًا للحالات لتحسين إمكانية القراءة.
الوحدة هي وحدة واحدة للسلوك، ويمكن أن تكون عدة فئات مترابطة.
يجب عزل كل اختبار عن الآخرين. لذلك يجب أن يكون من الممكن استدعاؤها بالتوازي أو بأي ترتيب.
يمتد TestExample من الفئة النهائية إلى TestCase {/** * @test */public function Suspension_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = new CanAlwaysSuspendPolicy();$sut = new Subscription();$result = $sut->suspend($canAlwaysSuspendPolicy);self::assertTrue($result);self::assertSame(Status::suspend(), $sut->status()); } }
الوحدة هي فئة واحدة.
يجب أن تكون الوحدة معزولة عن جميع المتعاونين.
يمتد TestExample من الفئة النهائية إلى TestCase {/** * @test */public function Suspension_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = $this->createStub(SuspendingPolicyInterface::class);$canAlwaysSuspendPolicy->method('suspend')->willReturn(true);$ سوت = new Subscription();$result = $sut->suspend($canAlwaysSuspendPolicy);self::assertTrue($result);self::assertSame(Status::suspend(), $sut->status()); } }
ملحوظة
النهج الكلاسيكي أفضل لتجنب الاختبارات الهشة.
[المهام]
مثال:
خدمة إعلام الدرجة النهائية {وظيفة عامة __construct(خاصة للقراءة فقط MailerInterface $mailer، خاصة للقراءة فقط messageRepositoryInterface $messageRepository) {}وظيفة عامة send(): void{$messages = $this->messageRepository->getAll();foreach ($messages as $message) { $this->mailer->send($message); } } }
[!تحذير|النمط:مسطح|التسمية:سيء]
يؤدي تأكيد التفاعلات مع بذرة إلى اختبارات هشة
يمتد TestExample من الفئة النهائية إلى TestCase {/** * @test */public function sends_all_notifications(): void{$message1 = new message();$message2 = new message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->method('getAll')->willReturn([$message1, $message2]);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$messageRepository->expects(self::once())->method('getAll');$mailer- >يتوقع(self::بالضبط(2))->method('send') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|النمط:مسطح|التسمية:جيد]
يمتد TestExample من الفئة النهائية إلى TestCase {/** * @test */public function sends_all_notifications(): void{$message1 = new message();$message2 = new message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->حفظ($message2);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);// تمت إزالة التفاعلات المؤكدة مع كعب الروتين$mailer->expects(self::exactly(2))->method ('يرسل') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|النمط:مسطح|التسمية:حتى أفضل في استخدام التجسس]
يمتد TestExample من الفئة النهائية إلى TestCase {/** * @test */public function sends_all_notifications(): void{$message1 = new message();$message2 = new message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->حفظ($message2);$mailer = new SpyMailer();$sut = new NotificationService($mailer, $messageRepository);$sut->send(); $mailer->assertThatMessagesHaveBeenSent([$message1, $message2]); } }
[!TIP|النمط:مسطح|التسمية:الخيار الأفضل]
أفضل مقاومة لإعادة البناء
أفضل دقة
أقل تكلفة للصيانة
إذا كان ذلك ممكنا، يجب أن تفضل هذا النوع من الاختبار
يمتد ExampalTest من الفئة النهائية إلى TestCase {/** * @test * @dataProvider getInvalidEmails */ الوظيفة العامة Detects_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( نتيجة $); }/** * @test * @dataProvider getValidEmails */ الوظيفة العامة Detects_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( نتيجة $); }الوظيفة العامة getInvalidEmails(): المصفوفة {return [ ['امتحان']، ['امتحان@']، ['test@test'],//...]; }الوظيفة العامة getValidEmails(): المصفوفة {return [ ["[email protected]"]، ['[email protected]']، ['[email protected]'],//...]; } }
[!تحذير|النمط:مسطح|التسمية:الخيار الأسوأ]
مقاومة أسوأ لإعادة البناء
دقة أسوأ
ارتفاع تكلفة الصيانة
يمتد ExampalTest من الفئة النهائية إلى TestCase {/** * @test */public functionAdd_an_item_to_cart(): void{$item = new CartItem('Product');$sut = new Cart();$sut->addItem($item);self::assertSame (1, $sut->getCount());self::assertSame($item, $sut->getItems()[0]); } }
[!تنبيه|النمط:مسطح|التسمية:الخيار الأسوأ]
أسوأ مقاومة لإعادة البناء
أسوأ دقة
أعلى تكلفة للصيانة
يمتد ExampalTest من الفئة النهائية إلى TestCase {/** * @test */public function sends_all_notifications(): void{$message1 = new message();$message2 = new message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->حفظ($message2);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('send') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!تحذير|النمط:مسطح|التسمية:سيء]
خدمة اسم الفئة النهائية {وظيفة عامة __construct(خاص للقراءة فقط CacheStorageInterface $cacheStorage) {}وظيفة عامة LoadAll(): void{$namesCsv = array_map('str_getcsv', file(__DIR__.'/../names.csv'));$names = [ ];foreach ($namesCsv كـ $nameData) {if (!isset($nameData[0], $nameData[1])) {continue; }$names[] = new Name($nameData[0], new Gender($nameData[1])); }$this->cacheStorage->store('names', $names); } }
كيفية اختبار رمز مثل هذا؟ يكون ذلك ممكنًا فقط من خلال اختبار التكامل لأنه يستخدم بشكل مباشر رمز البنية التحتية المتعلق بنظام الملفات.
[!TIP|النمط:مسطح|التسمية:جيد]
كما هو الحال في الهندسة الوظيفية، نحتاج إلى فصل التعليمات البرمجية ذات الآثار الجانبية والتعليمات البرمجية التي تحتوي على المنطق فقط.
الفئة النهائية NameParser {/** * @param array<string[]> $namesData * @return Name[] */public function parse(array $namesData): array{$names = [];foreach ($namesData as $nameData) {if (!isset($nameData[0], $nameData[1])) {continue; }$names[] = new Name($nameData[0], new Gender($nameData[1])); }إرجاع أسماء $؛ } }
الفئة النهائية CsvNamesFileLoader {تحميل الوظيفة العامة (): array{return array_map('str_getcsv', file(__DIR__.'/../names.csv')); } }
خدمة تطبيقات الدرجة النهائية {وظيفة عامة __construct(خاص للقراءة فقط CsvNamesFileLoader $fileLoader، خاص للقراءة فقط NameParser $parser، خاص للقراءة فقط CacheStorageInterface $cacheStorage) {}loadNames وظيفة عامة (): void{$namesData = $this->fileLoader->load();$names = $this->parse->parse($namesData);$this->cacheStorage->store('names', $names); } }
تعمل الفئة النهائية ValidUnitExampleTest على توسيع TestCase {/** * @test */الوظيفة العامة parse_all_names(): void{$namesData = [ ['جون'، 'م']، ['لينون'، 'U']، ['سارة'، 'W'] ];$sut = new NameParser();$result = $sut->parse($namesData); الذات::تأكيد نفسه( [اسم جديد('جون'، جنس جديد('M'))، اسم جديد('لينون'، جنس جديد('U'))، اسم جديد('سارة'، جنس جديد('W')) ],$النتيجة); } }
[!تحذير|النمط:مسطح|التسمية:سيء]
خدمة تطبيقات الدرجة النهائية {وظيفة عامة __construct(خاص للقراءة فقط SubscriptionRepositoryInterface $subscriptionRepository) {}وظيفة عامة RenewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);if (!$subscription->getStatus() ->isEqual(Status::expired())) {return خطأ شنيع؛ }$subscription->setStatus(Status::active());$subscription->setModifiedAt(new DateTimeImmutable());return true; } }
اشتراك الطبقة النهائية {وظيفة عامة __construct(حالة خاصة $status, Private DateTimeImmutable $modifiedAt) {}وظيفة عامة getStatus(): Status{return $this->status; }وظيفة عامة setStatus(Status $status): void{$this->status = $status; }الوظيفة العامة getModifiedAt(): DateTimeImmutable{return $this->modifiedAt; }الوظيفة العامة setModifiedAt(DateTimeImmutable $modifiedAt): void{$this->modifiedAt = $modifiedAt; } }
تقوم الفئة النهائية InvalidTestExample بتوسيع TestCase {/** * @test */public function Renew_an_expired_subscription_is_possible(): void{$modifiedAt = new DateTimeImmutable();$expiredSubscription = new Subscription(Status::expired(), $modifiedAt);$sut = new ApplicationService($this ->إنشاء مستودع($expiredSubscription));$result = $sut->renewSubscription(1);self::assertSame(Status::active(), $expiredSubscription->getStatus());self::assertGreaterThan($modifiedAt, $expiredSubscription->getModifiedAt());self:: AssureTrue($result); }/** * @test */public function Renew_an_active_subscription_is_not_possible(): void{$modifiedAt = new DateTimeImmutable();$activeSubscription = new Subscription(Status::active(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($activeSubscription));$result = $sut->renewSubscription(1);self::assertSame($modifiedAt, $activeSubscription->getModifiedAt());self::assertFalse($result); } وظيفة خاصة createRepository(الاشتراك $subscription): SubscriptionRepositoryInterface{إرجاع فئة جديدة ($expiredSubscription) تنفذ SubscriptionRepositoryInterface {وظيفة عامة __construct(اشتراك خاص للقراءة فقط $subscription) {} الوظيفة العامة findById(int $id): Subscription{return $this->subscription; } }; } }
[!TIP|النمط:مسطح|التسمية:جيد]
خدمة تطبيقات الدرجة النهائية {وظيفة عامة __construct(خاص للقراءة فقط SubscriptionRepositoryInterface $subscriptionRepository) {}وظيفة عامة RenewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);return $subscription->renew(new DateTimeImmutable( )); } }
اشتراك الطبقة النهائية {الحالة الخاصة $الحالة؛ DateTimeImmutable الخاص $modifiedAt؛ الوظيفة العامة __construct(DateTimeImmutable $modifiedAt) {$this->status = Status::new();$this->modifiedAt = $modifiedAt; } تجديد الوظيفة العامة (DateTimeImmutable $modifiedAt): bool{if (!$this->status->isEqual(Status::expired())) {return false; }$this->status = Status::active();$this->modifiedAt = $modifiedAt;return true; }وظيفة عامة نشطة(DateTimeImmutable $modifiedAt): void{//simplified$this->status = Status::active();$this->modifiedAt = $modifiedAt; } تنتهي صلاحية الوظيفة العامة (DateTimeImmutable $modifiedAt): void{//simplified$this->status = Status::expired();$this->modifiedAt = $modifiedAt; }الوظيفة العامة isActive(): bool{return $this->status->isEqual(Status::active()); } }
تعمل الفئة النهائية ValidTestExample على توسيع TestCase {/** * @test */public function Renew_an_expired_subscription_is_possible(): void{$expiredSubscription = SubscriptionMother::expired();$sut = new ApplicationService($this->createRepository($expiredSubscription));$result = $sut- >renewSubscription(1);// تخطي التحقق من التعديل لأنه ليس جزءًا مما يمكن ملاحظته سلوك. للتحقق من هذه القيمة، يجب علينا// إضافة حرف getter لـ ModedAt، ربما لأغراض الاختبار فقط.self::assertTrue($expiredSubscription->isActive());self::assertTrue($result); }/** * @test */public function Renew_an_active_subscription_is_not_possible(): void{$activeSubscription = SubscriptionMother::active();$sut = new ApplicationService($this->createRepository($activeSubscription));$result = $sut->renewSubscription(1);self::assertTrue($activeSubscription->isActive());self::assertFalse($result); } وظيفة خاصة createRepository(الاشتراك $subscription): SubscriptionRepositoryInterface{إرجاع فئة جديدة ($expiredSubscription) تنفذ SubscriptionRepositoryInterface {وظيفة عامة __construct(اشتراك خاص للقراءة فقط $subscription) {} الوظيفة العامة findById(int $id): Subscription{return $this->subscription; } }; } }
ملحوظة
نموذج الاشتراك الأول له تصميم سيء. لاستدعاء عملية تجارية واحدة، يتعين عليك الاتصال بثلاث طرق. كما أن استخدام الحروف للتحقق من العملية ليس ممارسة جيدة. في هذه الحالة، تم تخطي التحقق من تغيير modifiedAt
، ومن المحتمل أن يتم اختبار modifiedAt
محدد أثناء عملية التجديد من خلال عملية تجارية لانتهاء الصلاحية. إن getter لـ modifiedAt
غير مطلوب. بالطبع، هناك حالات يكون فيها العثور على إمكانية تجنب الحروف المقدمة للاختبارات فقط أمرًا صعبًا للغاية، ولكن يجب علينا دائمًا أن نحاول عدم تقديمها.
[!تحذير|النمط:مسطح|التسمية:سيء]
تقوم الفئة CannotSuspendExpiredSubscriptionPolicy بتنفيذ SuspendingPolicyInterface {تعليق الوظيفة العامة (الاشتراك $subscription، DateTimeImmutable $at): bool{if ($subscription->isExpired()) {return false; }العودة الحقيقية؛ } }
فئة CannotSuspendExpiredSubscriptionPolicyTest تمتد إلى TestCase {/** * @test */public function it_returns_false_when_a_subscription_is_expired(): void{$policy = new CannotSuspendExpiredSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isExpired')->willReturn(true);self::assertFalse($policy->suspend($subscription, new DateTimeImmutable())); }/** * @test */public function it_returns_true_when_a_subscription_is_not_expired(): void{$policy = new CannotSuspendExpiredSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isExpired')->willReturn(false);self::assertTrue($policy->suspend($subscription, new DateTimeImmutable())); } }
تقوم الفئة CannotSuspendNewSubscriptionPolicy بتنفيذ SuspendingPolicyInterface {تعليق الوظيفة العامة (الاشتراك $subscription، DateTimeImmutable $at): bool{if ($subscription->isNew()) {return false; }العودة الحقيقية؛ } }
فئة CannotSuspendNewSubscriptionPolicyTest تمتد إلى TestCase {/** * @test */public function it_returns_false_when_a_subscription_is_new(): void{$policy = new CannotSuspendNewSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isNew')->willReturn(true);self::assertFalse($policy->suspend($subscription, new DateTimeImmutable())); }/** * @test */public function it_returns_true_when_a_subscription_is_not_new(): void{$policy = new CannotSuspendNewSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isNew')->willReturn(false);self::assertTrue($policy->suspend($subscription, new DateTimeImmutable())); } }
تقوم فئة CanSuspendAfterOneMonthPolicy بتنفيذ SuspendingPolicyInterface {تعليق الوظيفة العامة (الاشتراك $subscription، DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
تعمل فئة CanSuspendAfterOneMonthPolicyTest على توسيع TestCase {/** * @test */public function it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable('2021-01-29');$policy = new CanSuspendAfterOneMonthPolicy();$subscription = new Subscription(new DateTimeImmutable('2020-12-28'));self::assertTrue($policy->suspend($subscription, $date)); }/** * @test */public function it_returns_false_when_a_subscription_is_not_older_than_one_month(): void{$date = new DateTimeImmutable('2021-01-29');$policy = new CanSuspendAfterOneMonthPolicy();$subscription = new Subscription(new DateTimeImmutable('2020-01-01'));self::assertTrue($policy->suspend($subscription, $date)); } }
حالة الفصل {private const EXPIRED = 'expired';private const ACTIVE = 'active';private const NEW = 'new';private const SUSPENDED = 'معلق';وظيفة خاصة __construct(سلسلة خاصة للقراءة فقط $status) {$this->status = $status; } انتهت صلاحية الوظيفة الثابتة العامة (): self{return new self(self::EXPIRED); }وظيفة ثابتة عامة نشطة (): self{return new self(self::ACTIVE); }وظيفة ثابتة عامة جديدة (): self{return new self(self::NEW); }وظيفة ثابتة عامة معلقة (): self{return new self(self::SUSPENDED); }الوظيفة العامة هيEqual(self $status): bool{return $this->status === $status->status; } }
يمتد فئة StatusTest إلى TestCase {وظيفة عامة testEquals(): void{$status1 = Status::active();$status2 = Status::active();self::assertTrue($status1->isEqual($status2)); } testNotEquals() الوظيفة العامة: void{$status1 = Status::active();$status2 = Status::expired();self::assertFalse($status1->isEqual($status2)); } }
يمتد اختبار الاشتراك في الفصل إلى TestCase {/** * @test */public function hanging_a_subscription_is_possible_when_a_policy_returns_true(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'suspend')->willReturn(true);$sut = new Subscription(new DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertTrue($result);self::assertTrue($sut->isSuspending()); }/** * @test */public function hanging_a_subscription_is_not_possible_when_a_policy_returns_false(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'suspend')->willReturn(false);$sut = new Subscription(new DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertFalse($result);self::assertFalse($sut->isSuspending()); }/** * @test */public function it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1M'));$sut = new Subscription($date);self::assertTrue($sut->isOlderThan($futureDate)); }/** * @test */public function it_returns_false_when_a_subscription_is_not_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1D'));$sut = new Subscription($date);self::assertTrue($sut->isOlderThan($futureDate)); } }
[!تنبيه] لا تكتب الكود 1:1، فئة واحدة: اختبار واحد. إنه يؤدي إلى اختبارات هشة مما يجعل إعادة البناء صعبة.
[!TIP|النمط:مسطح|التسمية:جيد]
الطبقة النهائية CannotSuspendExpiredSubscriptionPolicy تنفذ SuspendingPolicyInterface {تعليق الوظيفة العامة (الاشتراك $subscription، DateTimeImmutable $at): bool{if ($subscription->isExpired()) {return false; }العودة الحقيقية؛ } }
الطبقة النهائية CannotSuspendNewSubscriptionPolicy تنفذ SuspendingPolicyInterface {تعليق الوظيفة العامة (الاشتراك $subscription، DateTimeImmutable $at): bool{if ($subscription->isNew()) {return false; }العودة الحقيقية؛ } }
تقوم الفئة النهائية CanSuspendAfterOneMonthPolicy بتنفيذ SuspendingPolicyInterface {تعليق الوظيفة العامة (الاشتراك $subscription، DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
حالة الفصل النهائي {private const EXPIRED = 'expired';private const ACTIVE = 'active';private const NEW = 'new';private const SUSPENDED = 'معلق';وظيفة خاصة __construct(سلسلة خاصة للقراءة فقط $status) {$this->status = $status; } انتهت صلاحية الوظيفة الثابتة العامة (): self{return new self(self::EXPIRED); }وظيفة ثابتة عامة نشطة (): self{return new self(self::ACTIVE); }وظيفة ثابتة عامة جديدة (): self{return new self(self::NEW); }وظيفة ثابتة عامة معلقة (): self{return new self(self::SUSPENDED); }الوظيفة العامة هيEqual(self $status): bool{return $this->status === $status->status; } }
اشتراك الطبقة النهائية {الحالة الخاصة $status؛DateTimeImmutable الخاص $createdAt؛الوظيفة العامة __construct(DateTimeImmutable $createdAt) {$this->status = Status::new();$this->createdAt = $createdAt; }تعليق الوظيفة العامة (SuspendingPolicyInterface $suspendingPolicy, DateTimeImmutable $at): bool{$result = $suspendingPolicy->suspend($this, $at);if ($result) {$this->status = Status::suspending() ; }إرجاع نتيجة $; }الوظيفة العامة هيOlderThan(DateTimeImmutable $date): bool{return $this->createdAt < $date; }تنشيط الوظيفة العامة (): void{$this->status = Status::active(); }الوظيفة العامة تنتهي (): void{$this->status = Status::expired(); }الوظيفة العامة isExpired(): bool{return $this->status->isEqual(Status::expired()); }الوظيفة العامة isActive(): bool{return $this->status->isEqual(Status::active()); }الوظيفة العامة isNew(): bool{return $this->status->isEqual(Status::new()); }الوظيفة العامة isSuspending(): bool{return $this->status->isEqual(Status::suspending()); } }
يمتد SubscriptionSuspendingTest من الفئة النهائية إلى TestCase {/** * @test */public function Suspension_an_expired_subscription_with_cannot_suspend_expired_policy_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$sut->expire();$result = $sut ->تعليق (جديد CannotSuspendExpiredSubscriptionPolicy(), new DateTimeImmutable());self::assertFalse($result); }/** * @test */public function hanging_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable());$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy(), new DateTimeImmutable());self::assertFalse($result); }/** * @test */public function hanging_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy()); ، جديد DateTimeImmutable());self::assertTrue($result); }/** * @test */public function Suspension_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendExpiredSubscriptionPolicy()); ، جديد DateTimeImmutable());self::assertTrue($result); }/** * @test */public function hanging_an_subscription_before_a_one_month_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable('2020-01-01'));$result = $sut->suspend(new CanSuspendAfterOneMonthPolicy(), جديد DateTimeImmutable('2020-01-10'));self::assertFalse($result); }/** * @test */public function hanging_an_subscription_after_a_one_month_is_possible(): void{$sut = new Subscription(new DateTimeImmutable('2020-01-01'));$result = $sut->suspend(new CanSuspendAfterOneMonthPolicy(), جديد DateTimeImmutable('2020-02-02'));self::assertTrue($result); } }
كيفية وحدة اختبار فئة مثل هذا بشكل صحيح؟
خدمة تطبيقات الطبقة {وظيفة عامة __construct(خاص للقراءة فقط OrderRepository $orderRepository,خاص للقراءة فقط FormRepository $formRepository) {}تغيير الوظيفة العامةFormStatus(int $orderId): void{$order = $this->orderRepository->getById($orderId);$soapResponse = $this->getSoapClient()->getStatusByOrderId($orderId);$form = $this->formRepository->getByOrderId($orderId);$form->setStatus($soapResponse['status']);$form-> setModifiedAt(new DateTimeImmutable());if ($soapResponse['status'] === 'مقبول') {$order->setStatus('المدفوع'); }$this->formRepository->save($form);$this->orderRepository->save($order); }وظيفة خاصة getSoapClient(): SoapClient{return new SoapClient('https://legacy_system.pl/Soap/WebService', []); } }
[!TIP|النمط:مسطح|التسمية:جيد]
من الضروري تقسيم التعليمات البرمجية المعقدة إلى فئات منفصلة.
خدمة تطبيقات الدرجة النهائية {وظيفة عامة __construct (خاص للقراءة فقط OrderRepositoryInterface $orderRepository، خاص للقراءة فقط FormRepositoryInterface $formRepository، خاص للقراءة فقط FormApiInterface $formApi، خاص للقراءة فقط ChangeFormStatusService $changeFormStatusService) {}تغيير الوظيفة العامةFormStatus(int $orderId): void{$order = $this->orderRepository->getById($orderId);$form = $this->formRepository->getByOrderId($orderId);$status = $this->formApi->getStatusByOrderId($orderId);$this->changeFormStatusService ->changeStatus($order، $form، $status);$this->formRepository->save($form);$this->orderRepository->save($order); } }
الفئة النهائية ChangeFormStatusService {وظيفة عامة ChangeStatus(Order $order, Form $form, string $formStatus): void{$status = FormStatus::createFromString($formStatus);$form->changeStatus($status);if ($form->isAccepted( )) {$order->changeStatus(OrderStatus::pay()); } } }
يمتد الفصل الأخير ChangingFormStatusTest إلى TestCase {/** * @test */وظيفة عامة Changing_a_form_status_to_accepted_changes_an_order_status_to_pay(): void{$order = new Order();$form = new Form();$status = 'accepted';$sut = new ChangeFormStatusService();$sut ->changeStatus($order، $form، $status);self::assertTrue($form->isAccepted());self::assertTrue($order->isPaid()); }/** * @test */وظيفة عامة Changing_a_form_status_to_refused_not_changes_an_order_status(): void{$order = new Order();$form = new Form();$status = 'new';$sut = new ChangeFormStatusService();$sut ->changeStatus($order، $form، $status);self::assertFalse($form->isAccepted());self::assertFalse($order->isPaid()); } }
ومع ذلك، من المحتمل أن يتم اختبار ApplicationService عن طريق اختبار التكامل باستخدام FormApiInterface الساخرة فقط.
[!تحذير|النمط:مسطح|التسمية:سيء]
عميل الدرجة النهائية {وظيفة عامة __construct(سلسلة خاصة $name) {}وظيفة عامة getName(): سلسلة{return $this->name; }setName الوظيفة العامة (سلسلة $name): void{$this->name = $name; } }
يقوم CustomerTest من الدرجة النهائية بتوسيع TestCase {وظيفة عامة testSetName(): void{$customer = new Customer('Jack');$customer->setName('John');self::assertSame('John', $customer->getName()); } }
مشترك الحدث من الدرجة النهائية {وظيفة ثابتة عامة getSubscribedEvents(): array{return ['event' => 'onEvent']; }وظيفة عامة onEvent (): باطلة { } }
يمتد EventSubscriberTest من الفئة النهائية إلى TestCase {وظيفة عامة testGetSubscribedEvents(): void{$result = EventSubscriber::getSubscribedEvents();self::assertSame(['event' => 'onEvent'], $result); } }
[!تنبيه] اختبار الكود بدون أي منطق معقد هو أمر لا معنى له، ولكنه يؤدي أيضًا إلى اختبارات هشة.
[!تحذير|النمط:مسطح|التسمية:سيء]
مستودع المستخدم من الدرجة النهائية {وظيفة عامة __construct(اتصال خاص للقراءة فقط $connection) {}وظيفة عامة getUserNameByEmail(string $email): ?array{return $this->connection->createQueryBuilder() ->من ('المستخدم'، 'u') ->أين('u.email = :email') ->setParameter('email', $email) ->تنفيذ() ->جلب(); } }
تعمل الطبقة النهائية TestUserRepository على توسيع TestCase {وظيفة عامة testGetUserNameByEmail(): void{$email = '[email protected]';$connection = $this->createMock(Connection::class);$queryBuilder = $this->createMock(QueryBuilder::class); $result = $this->createMock(ResultStatement::class);$userRepository = جديد UserRepository($connection);$connection->يتوقع($this->once()) ->الطريقة('createQueryBuilder') ->willReturn($queryBuilder);$queryBuilder->يتوقع($this->once()) ->الطريقة("من") ->مع ('المستخدم'، 'u') ->willReturn($queryBuilder);$queryBuilder->يتوقع($this->once()) -> الطريقة ("أين") ->مع ('u.email = :email') ->willReturn($queryBuilder);$queryBuilder->يتوقع($this->once()) ->الطريقة('setParameter') ->مع ('البريد الإلكتروني'، البريد الإلكتروني $) ->willReturn($queryBuilder);$queryBuilder->يتوقع($this->once()) -> الطريقة ("تنفيذ") -> willreturn ($ result) ؛ $ result-> تتوقع ($ this-> مرة واحدة ()) -> الطريقة ('جلب') -> willreturn (['البريد الإلكتروني' => $ email]) ؛ $ result = $ userrepository-> getUserNameByMail ($ email) ؛ self :: assertSame (['email' => $ email] ، $ result) ؛ } }
[!تنبيه] يؤدي اختبار المستودعات بهذه الطريقة إلى اختبارات هشة ومن ثم تكون عملية إعادة البناء صعبة. لاختبار المستودعات، اكتب اختبارات التكامل.
[!TIP|النمط:مسطح|التسمية:جيد]
يمتد الفئة النهائية بشكل جيد {private succriptionfactory $ sut ؛ public function setup (): void {$ this-> sut = new cupcriptionFactory () ؛ }/** * test */function public creates_a_subscription_for_a_given_date_range (): void {$ result = $ this-> sut-> create (new DateTimeMmutable () ، dateTimeMutable جديد ('الآن +1 سنة')) ؛ (الاشتراك :: الفصل ، نتيجة $) ؛ }/** * test */public function throws_an_exception_on_invalid_date_range (): void {$ this-> teredexception (createSubscriptionException :: class) ؛ $ result = $ this-> sut-> Create (New DateTimeMutable ('الآن -1 year') ، new DateTimeMmutable ()) ؛ } }
ملحوظة
أفضل حالة لاستخدام طريقة الإعداد هي اختبار الكائنات عديمة الحالة.
أي تكوين يتم إجراؤه داخل setUp
يختبر الأزواج معًا، ويكون له تأثير على جميع الاختبارات.
من الأفضل تجنب الحالة المشتركة بين الاختبارات وتكوين الحالة الأولية وفقًا لطريقة الاختبار.
سهولة القراءة أسوأ مقارنة بالتكوين الذي تم إجراؤه بطريقة الاختبار المناسبة.
[!TIP|النمط:مسطح|التسمية:أفضل]
يمتد اختبار الفئة النهائية باختصار Testcase {/** * test */function public compling_an_active_subscription_with_cannot_suspend_new_policy_is_is_possible (): void {$ sut = $ this-> createanactiveSubscription () : AssertTrue (نتيجة $) ؛ }/** * test */وظيفة عامة compling_an_active_subscription_with_cannot_suspend_expired_policy_is_is_possible (): void {$ sut = $ this-> createanactivesubscription () dateTimeMmutable ()) ؛ Self :: AssertTrue ($ result) ؛ }/** * test */وظيفة عامة compling_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible () dateTimeImMutable ()) ؛ Self :: AssertFalse (نتيجة $) ؛ } الوظيفة الخاصة CreateAnewSubscription (): الاشتراك {إرجاع اشتراك جديد (جديد DateTimeMutable ()) ؛ } الوظيفة الخاصة CreateAnactiveSubscription (): الاشتراك {$ اشتراك = اشتراك جديد (جديد DateTimeMmutable ()) ؛ $ اشتراك-> Activate () ؛ إرجاع $ اشتراك ؛ } }
ملحوظة
يعمل هذا الأسلوب على تحسين إمكانية القراءة وتوضيح الفصل (يتم قراءة الكود أكثر من كتابته).
يمكن أن يكون استخدام المساعدين الخاصين مملاً في كل طريقة من طرق الاختبار، على الرغم من أنهم يقدمون نوايا واضحة.
لمشاركة كائنات اختبار مماثلة بين فئات اختبار متعددة، استخدم:
أم الكائن
منشئ
[!تحذير|النمط:مسطح|التسمية:سيء]
عميل الفصل النهائي {private customertype $ type ؛ discountcalculationpolicyInterface $ discountcalcalculationpolicy ؛ public function __construct () {$ this-> type = customerType :: Normal () ؛ $ this-> discountCalculationPolicy = newDiscountPolicy () ؛ } الوظيفة العامة makevip (): void {$ this-> type = customerType :: vip () ؛ $ this-> discountCalculationPolicy = new VipDiscountPolicy () ؛ } الوظيفة العامة getCustomerType (): customerType {return $ this-> type ؛ } الوظيفة العامة getPercentageScount (): int {return $ this-> discountCalculationPolicy-> getPercentagediscount () ؛ } }
الفئة النهائية غير صالحة يمتد testcase {public function testmakevip (): void {$ sut = new customer () ؛ $ sut-> makevip () ؛ self :: assertSame (customerType :: vip () ، $ sut-> getCustomerType ()) ؛ } }
[!TIP|النمط:مسطح|التسمية:جيد]
عميل الفصل النهائي {private customertype $ type ؛ discountcalculationpolicyInterface $ discountcalcalculationpolicy ؛ public function __construct () {$ this-> type = customerType :: Normal () ؛ $ this-> discountCalculationPolicy = newDiscountPolicy () ؛ } الوظيفة العامة makevip (): void {$ this-> type = customerType :: vip () ؛ $ this-> discountCalculationPolicy = new VipDiscountPolicy () ؛ } الوظيفة العامة getPercentageScount (): int {return $ this-> discountCalculationPolicy-> getPercentagediscount () ؛ } }
الفئة النهائية صالحة تمتد اختبار testcase {/** * test */public function a_vip_customer_has_a_25_percentage_discount (): void {$ sut = new customer () ؛ $ sut-> makevip () ؛ } }
[!تنبيه] تعد إضافة كود إنتاج إضافي (على سبيل المثال getCustomerType()) فقط للتحقق من الحالة في الاختبارات ممارسة سيئة. يجب التحقق منه بواسطة قيمة أخرى مهمة للمجال (في هذه الحالة getPercentageDiscount()). بالطبع، قد يكون من الصعب أحيانًا العثور على طريقة أخرى للتحقق من العملية، وقد نضطر إلى إضافة رمز إنتاج إضافي للتحقق من صحته في الاختبارات، ولكن يجب أن نحاول تجنب ذلك.
الفصل النهائي Discouncalculator {احسب الوظيفة العامة (int $ isVipfromyears): int { Assert :: Greaterthaneq ($ isVipfromyears ، 0) ؛ return min (($ isVipfromyears * 10) + 3 ، 80) ؛ } }
[!تحذير|النمط:مسطح|التسمية:سيء]
الفئة النهائية غير صالحة يمتد testcase {/** * dataprovider discountDataprovider */public function testCalculate (int $ vipdaysfrom ، int $ متوقع): void {$ sut = new DiscountCalculator () ؛ ); } دالة عامة DiscountDataprovider (): Array {Return [ [0 ، 0 * 10 + 3] ، // تفاصيل المجال المتسربة [1 ، 1 * 10 + 3] ، [5 ، 5 * 10 + 3] ، [8 ، 80] ]; } }
[!TIP|النمط:مسطح|التسمية:جيد]
الفئة النهائية صالحة تمتد اختبار testcase {/** * dataprovider discountDataprovider */public function testCalculate (int $ vipdaysfrom ، int $ متوقع): void {$ sut = new DiscountCalculator () ؛ ); } دالة عامة DiscountDataprovider (): Array {Return [ [0 ، 3] ، [1 ، 13] ، [5 ، 53] ، [8 ، 80] ]; } }
ملحوظة
لا تكرر منطق الإنتاج في الاختبارات. ما عليك سوى التحقق من النتائج من خلال القيم المشفرة.
[!تحذير|النمط:مسطح|التسمية:سيء]
الفصل الدراسي {الوظيفة العامة حساب internaldiscount (int $ isVipfromyears): int { Assert :: Greaterthaneq ($ isVipfromyears ، 0) ؛ return min (($ isVipfromyears * 10) + 3 ، 80) ؛ } الوظيفة العامة calculateadDitionalStIntFromexternalSystem (): int {// الحصول على بيانات من نظام خارجي لحساب discountreturn 5 ؛ } }
OrderService {الوظيفة العامة __construct (private readonly discountcalculator $ discountCalculator) {} وظيفة عامة getTotalPriceWithDiscount (int $ totalPrice ، int $ vipfromdays): int {$ internaldiscount = $ this-> discountcalculator-> calcutionInternaldisCount ($ vipfromdays) ؛ $ this-> discountCalculator-> compalateadditionaldiscountfromexternalsystem () ؛ $ disagedsum = $ internaldiscount + $ externaldiscount ؛ return $ totalPrice-(int) ceil (($ totalPrice * $ DisedSum) / 100) ؛ } }
الفئة النهائية غير صالحة يمتد testcase {/** * dataprovider orderdataprovider */public function testgetToTalPriceWithDiscount (int $ totalPrice ، int $ vipdaysfrom ، int $ متوقع): void {$ discountcalulator = $ this-> createPartialMock (discountCalculator :: class ، ['complateadditionaldiscountfromexternalsystem']) , $ vipdaysfrom)) ؛ } الوظيفة العامة OrderDataprovider (): Array {return [ [1000 ، 0 ، 920] ، [500 ، 1 ، 410] ، [644 ، 5 ، 270] ، ]; } }
[!TIP|النمط:مسطح|التسمية:جيد]
واجهة externaldiscountCalculatorInterface {الوظيفة العامة حساب (): int ؛ }
الفئة النهائية internaldiscountcalculator {احسب الوظيفة العامة (int $ isVipfromyears): int { Assert :: Greaterthaneq ($ isVipfromyears ، 0) ؛ return min (($ isVipfromyears * 10) + 3 ، 80) ؛ } }
أوامر الصف النهائي {الوظيفة العامة __construct (private readonly InternalDiscountCalculator $ DiscountCalculator ، private exterondaliscountCalulativeInterface $ externaldiscountcalculator) {} public getTotalPriceWithDiscount (int $ totalpric $ this-> discountcalculator-> حساب ($ vipfromDays) ؛ $ externaldiscount = $ this-> externaldiscountcalculator-> calcution () ؛ $ discounsum = $ internaldiscount + $ externaldiscount ؛ $ $ totalprice-(int) ceil (($ totalprice * $ * $ خصومات) / 100) ؛ } }
الفئة النهائية صالحة تمتد اختبار testcase {/** * dataprovider orderdataprovider */public function testgetToTalPriceWithDiscount (int $ totalPrice ، int $ vipdaysfrom ، int $ متوقع): void {$ externaldiscountcalculator = new class () externaldiscountculatileTface {public calcution (): int {return 5 ؛ } } ؛ $ sut = new orderservice (new InternalDiscountCalculator () ، $ externaldiscountcalculator) ؛ self :: assertSame ($ متوقع ، $ sut-> getTotalPriceWithDiscount ($ totalprice ، $ vipdaysfrom)) ؛ } الوظيفة العامة OrderDataprovider (): Array {return [ [1000 ، 0 ، 920] ، [500 ، 1 ، 410] ، [644 ، 5 ، 270] ، ]; } }
ملحوظة
إن ضرورة الاستهزاء بطبقة محددة لاستبدال جزء من سلوكها يعني أن هذه الطبقة ربما تكون معقدة للغاية وتنتهك مبدأ المسؤولية الفردية.
الطبقة النهائية OrderItem {الوظيفة العامة __construct (إجمالي readonly int $) {} }
أمر الطبقة النهائية {/** * param orderitem [] $ heats * param int $ transportcost */public function __construct (private Array $ heats ، private int $ transportcost) {} public function getTotal (): int {return $ this-> getItemStotal () + $ this-> transporcost ؛ } الوظيفة الخاصة getItemStotal (): int {return array_reduce (array_map (fn (orderitem $ item) => $ item-> total ، $ this-> العناصر) ، fn (int $ sum ، int $ total) => $ sum + = إجمالي $ ، 0) ؛ } }
[!تحذير|النمط:مسطح|التسمية:سيء]
الفئة النهائية غير صالحة يمتد testcase {/** * test * dataprovider ordersDataprovider */public function get_total_returns_a_total_cost_of_a_whole_order (order $ order ، int $ expectedTotal): void {self :: assertsame ($ repurittotal ، $ order-> gettotal () ؛ }/** * test * dataprovider orderitemsdataprovider */public function get_items_total_returns_a_total_cost_of_all_items (order $ order ، int $ felecttotal): void {self :: $ precideTtal ، $ that- } الوظيفة العامة ordersDataprovider (): Array {return [ [طلب جديد ([New Orderitem (20) ، New OrderItem (20) ، New OrderItem (20)] ، 15) ، 75] ، [طلب جديد ([New Orderitem (20) ، New OrderItem (30) ، New OrderItem (40)] ، 0) ، 90] ، [طلب جديد ([New Orderitem (99) ، New OrderItem (99) ، New OrderItem (99)] ، 9) ، 306] ]; } الوظيفة العامة OrderItemSdataprovider (): Array {return [ [طلب جديد ([New Orderitem (20) ، New OrderItem (20) ، New OrderItem (20)] ، 15) ، 60] ، [طلب جديد ([New Orderitem (20) ، New OrderItem (30) ، New OrderItem (40)] ، 0) ، 90] ، [طلب جديد ([New Orderitem (99) ، New OrderItem (99) ، New OrderItem (99)] ، 9) ، 297] ]; } الوظيفة الخاصة invokePrivateMethOdGetItEmStotal (Order & $ Order): int {$ Reflection = new ReflectionClass (get_class ($ order)) ؛ $ method = $ reflection-> getMethod ('getItemStotal') ؛ $ method-> setAccessible (true) ؛ $ method-> InvokeArgs ($ order ، []) ؛ } }
[!TIP|النمط:مسطح|التسمية:جيد]
الفئة النهائية صالحة تمتد اختبار testcase {/** * test * dataprovider ordersDataprovider */public function get_total_returns_a_total_cost_of_a_whole_order (order $ order ، int $ expectedTotal): void {self :: assertsame ($ repurittotal ، $ order-> gettotal () ؛ } الوظيفة العامة ordersDataprovider (): Array {return [ [طلب جديد ([New Orderitem (20) ، New OrderItem (20) ، New OrderItem (20)] ، 15) ، 75] ، [طلب جديد ([New Orderitem (20) ، New OrderItem (30) ، New OrderItem (40)] ، 0) ، 90] ، [طلب جديد ([New Orderitem (99) ، New OrderItem (99) ، New OrderItem (99)] ، 9) ، 306] ]; } }
[!تنبيه] يجب أن تتحقق الاختبارات من واجهة برمجة التطبيقات العامة فقط.
الوقت هو تبعية متقلبة لأنه غير حتمية. كل استدعاء يعود بنتيجة مختلفة.
[!تحذير|النمط:مسطح|التسمية:سيء]
ساعة الفصل النهائي {public static dateTime | null $ currentDateTime = null ؛ وظيفة ثابتة عامة getCurrentDateTime (): dateTime {if (null === self :: $ currentDateTime) {self :: $ currentDateTime = new dateTime () ؛ } إرجاع الذات :: $ currentDateTime ؛ } مجموعة الوظائف الثابتة العامة (DateTime $ DateTime): void {self :: $ currentDateTime = $ dateTime ؛ } إعادة تعيين الوظيفة الثابتة العامة (): void {self :: $ currentDateTime = null ؛ } }
عميل الفصل النهائي {private dateTime $ createAt ؛ public function __construct () {$ this-> createat = clock :: getCurrentDateTime () ؛ } الدالة العامة ISVIP (): bool {return $ this-> createat-> diff (clock :: getCurrentDateTime ())-> y> = 1 ؛ } }
الفئة النهائية غير صالحة يمتد testcase {/** * test */public function a_customer_registered_more_than_a_one_year_ago_is_a_vip (): void { clock :: set (dateTime new ('2019-01-01')) ؛ $ sut = new customer () ؛ Clock :: Reset () ؛ // عليك أن تتذكر إعادة تعيين StateSelf :: AssertTrue ($ sut-> isVip ()) ؛ }/** * test */public function a_customer_registered_less_than_a_one_year_ago_is_not_a_vip (): void { clock :: set ((new dateTime ())-> sub (dateInterval new ('p2m'))) ؛ $ sut = new customer () ؛ Clock :: Reset () ؛ // عليك أن تتذكر إعادة تعيين States STATESESS :: AssertFalse ($ sut-> isVip ()) ؛ } }
[!TIP|النمط:مسطح|التسمية:جيد]
واجهة clockinterface {الوظيفة العامة getCurrentTime (): dateTimeImMutable ؛ }
ساعة الفصل النهائي تنفذ واجهة الساعة {الوظيفة الخاصة __construct () { } الوظيفة الثابتة العامة Create (): self {return new Self () ؛ } الوظيفة العامة getCurrentTime (): DateTimeImMutable {return new DateTimeImMutable () ؛ } }
الفئة النهائية الثابتة تنفذ واجهة clockinter {الوظيفة الخاصة __construct (private readonly dateTimeImMutable $ ثابتة) {} create static static create (dateTimeImMutable $ ثابتة): self {return new self ($ fixeddate) ؛ } الوظيفة العامة getCurrentTime (): dateTimeImMutable {return $ this-> fixedDate ؛ } }
عميل الفصل النهائي {الوظيفة العامة __construct (private readonly dateTimeImMutable $ createAt) {} الوظيفة العامة isVip (dateTimeImMutable $ currentDate): bool {return $ this-> createat-> diff ($ currentDate)-> y> = 1 ؛ } }
الفئة النهائية صالحة تمتد اختبار testcase {/** * test */public function a_customer_registered_more_than_a_one_year_ago_is_a_vip (): void {$ sut = new customer (flexclock :: create (جديد (جديد DateTimeImMutable ('2019-01-01' ')))-> getCurrentTime ()) ؛ self :: assertTrue ($ sut-> isVip (flexclock :: create (new DateTimeMmutable (' 2020-01-02 ')-> getCurrentTime ( )) ؛ ستر DateTimeImMutable ('2019-01-01' ')))-> getCurrentTime ()) ؛ Self :: AsserTfalse ($ sut-> isVip (fixedclock :: create (new DateTimeMutable (' 2019-05-02 ')-> getCurrentTime ( )) ؛ } }
ملحوظة
لا ينبغي إنشاء الوقت والأرقام العشوائية مباشرة في رمز المجال. لاختبار السلوك يجب أن نحصل على نتائج حتمية، لذلك نحتاج إلى إدخال هذه القيم في كائن المجال كما في المثال أعلاه.
التغطية بنسبة 100% ليست هي الهدف أو حتى غير مرغوب فيها لأنه إذا كانت هناك تغطية بنسبة 100%، فمن المحتمل أن تكون الاختبارات هشة للغاية، مما يعني أن إعادة الهيكلة ستكون صعبة للغاية. يعطي اختبار الطفرة تعليقات أفضل حول جودة الاختبارات. اقرأ المزيد
التنمية التي تحركها الاختبار: على سبيل المثال / Kent Beck - The Classic
مبادئ وممارسات وأنماط اختبار الوحدة / فلاديمير خوريكوف - أفضل كتاب قرأته عن الاختبارات
كميل روكزينسكي
تويتر: https://twitter.com/Sarvendev
المدونة: https://sarvendev.com/
ينكدين: https://www.linkedin.com/in/kamilruczynski/