ในยุคนี้ การเขียน Unit Test มีประโยชน์มากมาย ฉันคิดว่าโปรเจ็กต์ที่เพิ่งเริ่มต้นส่วนใหญ่มีการทดสอบหน่วยใดๆ ในแอปพลิเคชันระดับองค์กรที่มีตรรกะทางธุรกิจจำนวนมาก การทดสอบหน่วยเป็นการทดสอบที่สำคัญที่สุด เนื่องจากรวดเร็วและเราสามารถรับประกันได้ทันทีว่าการใช้งานของเรานั้นถูกต้อง อย่างไรก็ตาม ฉันมักจะเห็นปัญหากับการทดสอบที่ดีในโครงการ แม้ว่าประโยชน์ของการทดสอบเหล่านี้จะยิ่งใหญ่ก็ต่อเมื่อคุณมีการทดสอบหน่วยที่ดีเท่านั้น ในตัวอย่างนี้ ฉันจะพยายามแบ่งปันเคล็ดลับบางประการเกี่ยวกับสิ่งที่ต้องทำเพื่อเขียน Unit Tests ที่ดี
เวอร์ชันอ่านง่าย: https://testing-tips.sarvendev.com/
คามิล รุคซินสกี้
บล็อก: https://sarvendev.com/
LinkedIn: https://www.linkedin.com/in/kamilruczynski/
การสนับสนุนของคุณหมายถึงโลกสำหรับฉัน! หากคุณชอบคำแนะนำนี้และพบคุณค่าในความรู้ที่แบ่งปัน ลองสนับสนุนฉันที่ BuyMeCoffee:
หรือเพียงทิ้งดาวไว้บนพื้นที่เก็บข้อมูลและติดตามฉันบน Twitter และ Github เพื่อรับทราบข้อมูลอัปเดตทั้งหมด ความมีน้ำใจของคุณทำให้ฉันหลงใหลในการสร้างเนื้อหาที่ลึกซึ้งมากขึ้นสำหรับคุณ
หากคุณมีแนวคิดในการปรับปรุงหรือหัวข้อที่จะเขียน โปรดเตรียมคำขอดึงหรือแจ้งให้เราทราบ
สมัครสมาชิกและทดสอบหน่วยหลักด้วย eBook ฟรีของฉัน!
รายละเอียด
ฉันยังมีรายการการปรับปรุง TODO ที่ค่อนข้างยาวสำหรับคู่มือนี้เกี่ยวกับการทดสอบหน่วย และฉันจะแนะนำพวกเขาในอนาคตอันใกล้นี้
การแนะนำ
ผู้เขียน
ทดสอบสองเท่า
การตั้งชื่อ
แบบ AAA
แม่วัตถุ
ช่างก่อสร้าง
ยืนยันวัตถุ
การทดสอบแบบกำหนดพารามิเตอร์
โรงเรียนทดสอบหน่วยสองแห่ง
คลาสสิค
คนชอบเยาะเย้ย
การพึ่งพาอาศัยกัน
เยาะเย้ยและ Stub
การทดสอบหน่วยสามรูปแบบ
เอาท์พุต
สถานะ
การสื่อสาร
สถาปัตยกรรมการทำงานและการทดสอบ
พฤติกรรมที่สังเกตได้เทียบกับรายละเอียดการใช้งาน
หน่วยของพฤติกรรม
รูปแบบที่ถ่อมตัว
การทดสอบเล็กน้อย
การทดสอบที่เปราะบาง
อุปกรณ์ทดสอบ
การทดสอบต่อต้านรูปแบบทั่วไป
เปิดเผยสภาพเอกชน
รายละเอียดโดเมนรั่วไหล
การเยาะเย้ยชั้นเรียนที่เป็นรูปธรรม
ทดสอบวิธีการส่วนตัว
เวลาเป็นการพึ่งพาที่ผันผวน
ความครอบคลุมการทดสอบ 100% ไม่ควรเป็นเป้าหมาย
หนังสือแนะนำ
การทดสอบสองเท่าเป็นการพึ่งพาปลอมที่ใช้ในการทดสอบ
หุ่นจำลองเป็นเพียงการใช้งานง่ายๆ ที่ไม่ทำอะไรเลย
Mailer ระดับสุดท้ายใช้ MailerInterface {ฟังก์ชั่นสาธารณะส่ง (ข้อความ $ ข้อความ): เป็นโมฆะ { - -
ของปลอมเป็นการใช้งานที่ง่ายขึ้นเพื่อจำลองพฤติกรรมดั้งเดิม
InMemoryCustomerRepository คลาสสุดท้ายใช้ CustomerRepositoryInterface {/** * @var Customer[] */private array $customers;public function __construct() {$นี่->ลูกค้า = []; }ร้านค้าฟังก์ชั่นสาธารณะ (ลูกค้า $ ลูกค้า): เป็นโมฆะ {$this->ลูกค้า [(สตริง) $customer->id()->id()] = $ลูกค้า; }ฟังก์ชันสาธารณะ get(CustomerId $id): ลูกค้า{if (!isset($this->customers[(string) $id->id()])) {throw new CustomerNotFoundException(); }ส่งคืน $this->ลูกค้า[(สตริง) $id->id()]; }ฟังก์ชันสาธารณะ findByEmail(อีเมล $email): ลูกค้า{foreach ($this->ลูกค้าเป็น $customer) {if ($ลูกค้า->getEmail()->isEqual($email)) {return $customer; - } โยน CustomerNotFoundException ใหม่ (); - -
Stub คือการใช้งานที่ง่ายที่สุดโดยมีลักษณะการทำงานแบบฮาร์ดโค้ด
คลาสสุดท้าย UniqueEmailSpecificationStub ใช้ UniqueEmailSpecificationInterface { ฟังก์ชั่นสาธารณะ isUnique (อีเมล $ อีเมล): บูล { ส่งคืนจริง; - -
$specialStub = $this->createStub(UniqueEmailSpecificationInterface::class);$specialStub->method('isUnique')->willReturn(true);
สายลับคือการดำเนินการเพื่อตรวจสอบพฤติกรรมเฉพาะ
Mailer ระดับสุดท้ายใช้ MailerInterface {/** * @var Message[] */อาร์เรย์ส่วนตัว $messages; ฟังก์ชั่นสาธารณะ __ สร้าง () {$นี่->ข้อความ = []; } ฟังก์ชั่นสาธารณะส่ง (ข้อความ $message): เป็นโมฆะ{$this->messages[] = $message; }ฟังก์ชันสาธารณะ getCountOfSentMessages(): int{return count($this->messages); - -
การเยาะเย้ยเป็นการเลียนแบบที่กำหนดค่าไว้เพื่อตรวจสอบการโทรของผู้ทำงานร่วมกัน
$message = new Message('[email protected]', 'Test', 'Test test test');$mailer = $this->createMock(MailerInterface::class);$mailer->expects($this-> ครั้งหนึ่ง()) ->วิธีการ('ส่ง') ->with($this->equalTo($message));
[!ความสนใจ] หากต้องการตรวจสอบการโต้ตอบที่เข้ามา ให้ใช้ stub แต่หากต้องการตรวจสอบการโต้ตอบที่ออกมา ให้ใช้การเยาะเย้ย
เพิ่มเติม: เยาะเย้ยกับ Stub
[!คำเตือน|style:flat|ป้ายกำกับ:ไม่ดี]
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([ตนเอง::equalTo($message1)], [self::equalTo($message2)]);$sut->ส่ง(); - -
[!TIP|style:flat|ป้ายกำกับ:ดีกว่า]
ต้านทานการรีแฟคเตอร์ได้ดีขึ้น
การใช้ 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->save($message2);$mailer = new SpyMailer();$sut = ใหม่ บริการแจ้งเตือน($mailer, $messageRepository);$sut->send(); $mailer->assertThatMessagesHaveBeenSent([$message1, $message2]); - -
[!คำเตือน|style:flat|ป้ายกำกับ:ไม่ดี]
การทดสอบฟังก์ชันสาธารณะ(): เป็นโมฆะ{$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 ()); -
[!คำเตือน|style:flat|ป้ายกำกับ:ไม่ดี]
ฟังก์ชั่นสาธารณะ it_throws_invalid_credentials_Exception_when_sign_in_with_invalid_credentials (): เป็นโมฆะ { } ฟังก์ชั่นสาธารณะ testCreatingWithATooShortPasswordIsNotPossible(): เป็นโมฆะ{ } ฟังก์ชั่นสาธารณะ testDeactivateASubscription (): เป็นโมฆะ { -
[!TIP|style:flat|ป้ายกำกับ:ดีกว่า]
การใช้ขีดล่างช่วยให้อ่านง่ายขึ้น
ชื่อควรอธิบายลักษณะการทำงาน ไม่ใช่การใช้งาน
ใช้ชื่อโดยไม่มีคำหลักทางเทคนิค ควรอ่านได้สำหรับผู้ที่ไม่ใช่โปรแกรมเมอร์
ฟังก์ชั่นสาธารณะ sign_in_with_invalid_credentials_is_not_possible (): เป็นโมฆะ { }การสร้างฟังก์ชันสาธารณะ_with_a_too_short_password_is_not_possible(): เป็นโมฆะ{ }ฟังก์ชั่นสาธารณะ deactivating_an_activated_subscription_is_valid(): เป็นโมฆะ{ }ฟังก์ชั่นสาธารณะ deactivating_an_inactive_subscription_is_invalid(): เป็นโมฆะ{ -
บันทึก
การอธิบายลักษณะการทำงานเป็นสิ่งสำคัญในการทดสอบสถานการณ์สมมติของโดเมน หากรหัสของคุณเป็นเพียงเครื่องมืออรรถประโยชน์ก็มีความสำคัญน้อยกว่า
เหตุใดจึงมีประโยชน์สำหรับผู้ที่ไม่ใช่โปรแกรมเมอร์ในการอ่านการทดสอบหน่วย
หากมีโปรเจ็กต์ที่มีตรรกะโดเมนที่ซับซ้อน ตรรกะนี้จะต้องมีความชัดเจนสำหรับทุกคน ดังนั้นการทดสอบจะอธิบายรายละเอียดโดเมนโดยไม่ต้องใช้คีย์เวิร์ดทางเทคนิค และคุณสามารถพูดคุยกับธุรกิจในภาษาเดียวกับในการทดสอบเหล่านี้ได้ รหัสทั้งหมดที่เกี่ยวข้องกับโดเมนไม่ควรมีรายละเอียดทางเทคนิค ผู้ที่ไม่ใช่โปรแกรมเมอร์จะไม่ถูกอ่านการทดสอบเหล่านี้ หากคุณต้องการพูดคุยเกี่ยวกับโดเมน การทดสอบเหล่านี้จะมีประโยชน์ในการทราบว่าโดเมนนี้ทำอะไร จะมีคำอธิบายที่ไม่มีรายละเอียดทางเทคนิค เช่น ส่งคืนค่าว่าง ส่งข้อยกเว้น เป็นต้น ข้อมูลประเภทนี้ไม่เกี่ยวข้องกับโดเมน ดังนั้นเราจึงไม่ควรใช้คีย์เวิร์ดเหล่านี้
นอกจากนี้ยังเป็นเรื่องปกติของ Give, When, That
แยกการทดสอบสามส่วน:
Arrange : นำระบบมาทดสอบในสถานะที่ต้องการ เตรียมการพึ่งพา ข้อโต้แย้ง และสร้าง SUT ในที่สุด
พระราชบัญญัติ : เรียกใช้องค์ประกอบที่ทดสอบ
Assert : ตรวจสอบผลลัพธ์ สถานะสุดท้าย หรือการสื่อสารกับผู้ทำงานร่วมกัน
[!TIP|style:flat|ป้ายกำกับ:GOOD]
ฟังก์ชั่นสาธารณะ aaa_pattern_example_test(): void{//Arrange|Given$sut = SubscriptionMother::new();//Act|When$sut->activate();//Assert|Thenself::assertSame(Status::activated( ), $sut->สถานะ()); -
รูปแบบนี้ช่วยในการสร้างวัตถุเฉพาะที่สามารถนำมาใช้ซ้ำได้ในการทดสอบเพียงไม่กี่ครั้ง ด้วยเหตุนี้ส่วนการจัดเรียงจึงกระชับและการทดสอบโดยรวมจึงสามารถอ่านได้ง่ายขึ้น
SubscriptionMother คลาสสุดท้าย { ฟังก์ชั่นคงที่สาธารณะ ใหม่ (): การสมัครสมาชิก { คืนการสมัครสมาชิกใหม่ (); } }เปิดใช้งานฟังก์ชันคงที่สาธารณะแล้ว(): การสมัครสมาชิก{$subscription = การสมัครสมาชิกใหม่();$subscription->เปิดใช้งาน();ส่งคืน $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 = [];ฟังก์ชันสาธารณะที่สร้างขึ้นAt(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, - - -
คลาสสุดท้าย ExampleTest ขยาย TestCase {/** * @test */ฟังก์ชันสาธารณะ example_test_with_order_builder(): void{$order = (new OrderBuilder()) ->createdAt(DateTimeImmutable ใหม่('2022-11-10 20:00:00')) ->withItem('รายการที่ 1', 1,000) ->withItem('รายการที่ 2', 2000) ->withItem('รายการ 3', 3000) ->build();// do someone// check something} -
รูปแบบวัตถุยืนยันช่วยเขียนส่วนยืนยันที่อ่านง่ายขึ้น แทนที่จะใช้การยืนยันเพียงเล็กน้อย เราสามารถเตรียมนามธรรม และใช้ภาษาธรรมชาติเพื่ออธิบายผลลัพธ์ที่คาดหวังได้
คลาสสุดท้าย ExampleTest ขยาย TestCase {/** * @test */ฟังก์ชันสาธารณะ example_test_with_asserter(): void{$currentTime = new DateTimeImmutable('2022-11-10 20:00:00');$sut = new OrderService();$order = $sut ->สร้าง($เวลาปัจจุบัน); OrderAsserter::assertThat($คำสั่งซื้อ) -> wasCreatedAt($เวลาปัจจุบัน) ->มีทั้งหมด(6000); - -
ใช้ PHPUnitFrameworkAssert; OrderAsserter คลาสสุดท้าย { ฟังก์ชั่นสาธารณะ __ สร้าง (คำสั่งซื้อแบบอ่านอย่างเดียวแบบส่วนตัว $ สั่งซื้อ) {} ฟังก์ชั่นคงที่สาธารณะ assertThat (สั่งซื้อ $ สั่งซื้อ): ตัวเอง { คืน OrderAsserter ใหม่ ($ สั่งซื้อ); }ฟังก์ชันสาธารณะ wasCreatedAt (DateTimeImmutable $createdAt): self{ Assert::assertEquals($createdAt, $this->order->createdAt); return $this; }ฟังก์ชั่นสาธารณะ hasTotal(int $total): self{ Assert::assertSame($total, $this->order->getTotal());return $this; - -
การทดสอบแบบกำหนดพารามิเตอร์เป็นตัวเลือกที่ดีในการทดสอบ SUT ด้วยพารามิเตอร์จำนวนมากโดยไม่ต้องทำซ้ำโค้ด
คำเตือน
การทดสอบประเภทนี้อ่านได้น้อย เพื่อเพิ่มความสามารถในการอ่าน ควรแยกตัวอย่างเชิงลบและบวกออกเป็นการทดสอบต่างๆ
ExampleTest คลาสสุดท้ายขยาย TestCase {/** * @test * @dataProvider getInvalidEmails */public function detectors_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( $ผลลัพธ์); }/** * @test * @dataProvider getValidEmails */public function detectors_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( $ผลลัพธ์); }ฟังก์ชันสาธารณะ getInvalidEmails(): iterable{yield 'An invalid email without @' => ['test'];yield 'An invalid email without the domain after @' => ['test@'];yield 'An invalid email ไม่มี TLD' => ['test@test'];//...}ฟังก์ชั่นสาธารณะ getValidEmails(): iterable{yield 'A valid email with lowercase ตัวอักษร' => ['[email protected]'];ให้ 'อีเมลที่ถูกต้องพร้อมตัวอักษรพิมพ์เล็กและตัวเลข' => ['[email protected]'];ให้ 'อีเมลที่ถูกต้องพร้อมตัวอักษรตัวพิมพ์ใหญ่และตัวเลข' => ['Test123@ test.com'];//...} -
บันทึก
ใช้ yield
และเพิ่มคำอธิบายข้อความให้กับกรณีและปัญหาเพื่อปรับปรุงความสามารถในการอ่าน
หน่วยนี้เป็นหน่วยพฤติกรรมหน่วยเดียว ซึ่งสามารถเป็นคลาสที่เกี่ยวข้องได้ 2-3 คลาส
การทดสอบทุกครั้งควรแยกจากการทดสอบอื่นๆ ดังนั้นจึงต้องสามารถเรียกใช้แบบขนานหรือในลำดับใดก็ได้
TestExample คลาสสุดท้ายขยาย TestCase {/** * @test */ฟังก์ชันสาธารณะ 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(สถานะ::suspend(), $sut->สถานะ()); - -
หน่วยเป็นชั้นเดียว
หน่วยนี้ควรแยกออกจากผู้ทำงานร่วมกันทั้งหมด
TestExample คลาสสุดท้ายขยาย TestCase {/** * @test */ฟังก์ชันสาธารณะ Suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = $this->createStub(SuspendingPolicyInterface::class);$canAlwaysSuspendPolicy->method('suspend')->willReturn(true);$ สุด = ใหม่ การสมัครสมาชิก();$result = $sut->suspend($canAlwaysSuspendPolicy);self::assertTrue($result);self::assertSame(สถานะ::suspend(), $sut->สถานะ()); - -
บันทึก
วิธีการแบบคลาสสิกจะดีกว่าเพื่อหลีกเลี่ยงการทดสอบที่เปราะบาง
[สิ่งที่ต้องทำ]
ตัวอย่าง:
บริการแจ้งเตือนคลาสสุดท้าย { ฟังก์ชั่นสาธารณะ __ สร้าง (ส่วนตัวอ่านอย่างเดียว MailerInterface $mailer, ส่วนตัวอ่านอย่างเดียว MessageRepositoryInterface $messageRepository) {}ฟังก์ชั่นสาธารณะ ส่ง (): เป็นโมฆะ{$messages = $this->messageRepository->getAll();foreach ($ข้อความเป็น $message) { $this->mailer->send($message); - - -
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
การยืนยันปฏิสัมพันธ์กับสตับนำไปสู่การทดสอบที่เปราะบาง
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- >expects(self::exactly(2))->method('send') ->withConsecutive([ตนเอง::equalTo($message1)], [self::equalTo($message2)]);$sut->ส่ง(); - -
[!TIP|style:flat|ป้ายกำกับ:GOOD]
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);// ลบการยืนยันการโต้ตอบกับ stub$mailer->expects(self::exactly(2))->method ออกแล้ว ('ส่ง') ->withConsecutive([ตนเอง::equalTo($message1)], [self::equalTo($message2)]);$sut->ส่ง(); - -
[!TIP|style:flat|label:ใช้ SPY ได้ดียิ่งขึ้น]
TestExample คลาสสุดท้ายขยาย TestCase {/** * @test */public function sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = new InMemoryMessageRepository();$messageRepository->save($message1) ;$messageRepository->save($message2);$mailer = new SpyMailer();$sut = ใหม่ บริการแจ้งเตือน($mailer, $messageRepository);$sut->send(); $mailer->assertThatMessagesHaveBeenSent([$message1, $message2]); - -
[!TIP|style:flat|label:ตัวเลือกที่ดีที่สุด]
ต้านทานการรีแฟคเตอร์ได้ดีที่สุด
ความแม่นยำที่ดีที่สุด
ต้นทุนการบำรุงรักษาต่ำที่สุด
หากเป็นไปได้ คุณควรเลือกการทดสอบประเภทนี้
คลาสสุดท้าย ExampleTest ขยาย TestCase {/** * @test * @dataProvider getInvalidEmails */public function detectors_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( $ผลลัพธ์); }/** * @test * @dataProvider getValidEmails */public function detectors_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]'],//...]; - -
[!คำเตือน|style:flat|label:ตัวเลือกที่แย่กว่า]
ความต้านทานต่อการปรับโครงสร้างใหม่แย่ลง
ความแม่นยำแย่ลง
ต้นทุนการบำรุงรักษาที่สูงขึ้น
คลาสสุดท้าย ExampleTest ขยาย TestCase {/** * @test */public function added_an_item_to_cart(): void{$item = new CartItem('Product');$sut = new Cart();$sut->addItem($item);self::assertSame (1, $sut->getCount());self::assertSame($item, $sut->getItems()[0]); - -
[!ATTENTION|style:flat|label:ตัวเลือกที่แย่ที่สุด]
ความต้านทานที่เลวร้ายที่สุดต่อการปรับโครงสร้างใหม่
ความแม่นยำที่เลวร้ายที่สุด
ต้นทุนการบำรุงรักษาสูงสุด
คลาสสุดท้าย ExampleTest ขยาย 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([ตนเอง::equalTo($message1)], [self::equalTo($message2)]);$sut->ส่ง(); - -
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
NameService คลาสสุดท้าย {ฟังก์ชั่นสาธารณะ __สร้าง (ส่วนตัวอ่านอย่างเดียว CacheStorageInterface $cacheStorage) {}ฟังก์ชั่นสาธารณะ loadAll(): void{$namesCsv = array_map('str_getcsv', file(__DIR__.'/../names.csv'));$names = [ ];foreach ($namesCsv เป็น $nameData) {if (!isset($nameData[0], $nameData[1])) {ดำเนินการต่อ; }$names[] = ชื่อใหม่($nameData[0], เพศใหม่($nameData[1])); }$this->cacheStorage->store('ชื่อ', $names); - -
จะทดสอบรหัสเช่นนี้ได้อย่างไร? เป็นไปได้เฉพาะกับการทดสอบการรวมระบบเท่านั้น เนื่องจากใช้โค้ดโครงสร้างพื้นฐานที่เกี่ยวข้องกับระบบไฟล์โดยตรง
[!TIP|style:flat|ป้ายกำกับ:GOOD]
เช่นเดียวกับในสถาปัตยกรรมเชิงฟังก์ชัน เราต้องแยกโค้ดที่มีผลข้างเคียงและโค้ดที่มีเพียงตรรกะเท่านั้น
NameParser คลาสสุดท้าย {/** * @param array<string[]> $namesData * @return Name[] */public function parse(array $namesData): array{$names = [];foreach ($namesData as $nameData) {if (!isset($nameData[0], $nameData[1])) {ดำเนินการต่อ; }$names[] = ชื่อใหม่($nameData[0], เพศใหม่($nameData[1])); } ส่งคืน $names; - -
CsvNamesFileLoader คลาสสุดท้าย { โหลดฟังก์ชันสาธารณะ(): array{return array_map('str_getcsv', file(__DIR__.'/../names.csv')); - -
ApplicationService คลาสสุดท้าย {ฟังก์ชันสาธารณะ __สร้าง (ส่วนตัวอ่านอย่างเดียว CsvNamesFileLoader $fileLoader, ส่วนตัวอ่านอย่างเดียว NameParser $parser, ส่วนตัวอ่านอย่างเดียว CacheStorageInterface $cacheStorage) {}ฟังก์ชันสาธารณะ loadNames(): void{$namesData = $this->fileLoader->load();$names = $this->parser->parse($namesData);$this->cacheStorage->store('ชื่อ', $names); - -
ValidUnitExampleTest คลาสสุดท้ายขยาย TestCase {/** * @test */ฟังก์ชันสาธารณะ parse_all_names(): void{$namesData = [ ['จอห์น', 'ม'], ['เลนนอน', 'คุณ'], ['ซาราห์', 'ว'] ];$sut = ใหม่ NameParser();$result = $sut->parse($namesData); ตนเอง::assertSame( [ชื่อใหม่('จอห์น' เพศใหม่('M')) ชื่อใหม่('เลนนอน' เพศใหม่('U')) ชื่อใหม่('ซาราห์' เพศใหม่('W')) ],$ผลลัพธ์); - -
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
ApplicationService คลาสสุดท้าย { ฟังก์ชั่นสาธารณะ __ สร้าง (ส่วนตัวอ่านอย่างเดียว SubscriptionRepositoryInterface $subscriptionRepository) {} ฟังก์ชั่นสาธารณะ renewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);if (!$subscription->getStatus() ->เท่ากับ(สถานะ::expired())) {return เท็จ; }$subscription->setStatus(Status::active());$subscription->setModifiedAt(new DateTimeImmutable());คืนค่าจริง; - -
การสมัครสมาชิกคลาสสุดท้าย { ฟังก์ชั่นสาธารณะ __construct (สถานะส่วนตัว $status, DateTimeImmutable $modifiedAt ส่วนตัว) {}ฟังก์ชั่นสาธารณะ getStatus(): สถานะ{return $this->สถานะ; }ฟังก์ชันสาธารณะ setStatus (สถานะ $status): โมฆะ{$this->status = $status; }ฟังก์ชันสาธารณะ getModifiedAt(): DateTimeImmutable{return $this->modifiedAt; } ฟังก์ชั่นสาธารณะ setModifiedAt (DateTimeImmutable $modifiedAt): เป็นโมฆะ{$this->modifiedAt = $modifiedAt; - -
คลาสสุดท้าย InvalidTestExample ขยาย TestCase {/** * @test */ฟังก์ชันสาธารณะ renew_an_expired_subscription_is_possible(): void{$modifiedAt = new DateTimeImmutable();$expiredSubscription = new Subscription(Status::expired(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($expiredSubscription));$result = $sut->renewSubscription(1);self::assertSame(สถานะ::active(), $expiredSubscription->getStatus());self::assertGreaterThan($modifiedAt, $expiredSubscription->getModifiedAt());self:: assertTrue($ผลลัพธ์); }/** * @test */public ฟังก์ชั่น 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 { ฟังก์ชั่นสาธารณะ __ สร้าง (การสมัครสมาชิกแบบอ่านอย่างเดียวส่วนตัว $ subscription) {} ฟังก์ชั่นสาธารณะ findById (int $id): การสมัครสมาชิก {return $ this-> การสมัครสมาชิก; - - - -
[!TIP|style:flat|ป้ายกำกับ:GOOD]
ApplicationService คลาสสุดท้าย { ฟังก์ชั่นสาธารณะ __ สร้าง (ส่วนตัวอ่านอย่างเดียว SubscriptionRepositoryInterface $subscriptionRepository) {} ฟังก์ชั่นสาธารณะ ต่ออายุสมัครสมาชิก (int $subscriptionId): บูล{$subscription = $this->subscriptionRepository->findById($subscriptionId); กลับ $subscription->ต่ออายุ (ใหม่ DateTimeImmutable( )); - -
การสมัครสมาชิกคลาสสุดท้าย {สถานะส่วนตัว $status;DateTimeImmutable $modifiedAt ส่วนตัว; ฟังก์ชั่นสาธารณะ __construct (DateTimeImmutable $modifiedAt) {$this->status = สถานะ::new();$this->modifiedAt = $modifiedAt; } ฟังก์ชั่นสาธารณะต่ออายุ (DateTimeImmutable $modifiedAt): bool{if (!$this->status->isEqual(Status::expired())) {return false; }$this->status = Status::active();$this->modifiedAt = $modifiedAt;return จริง; }ฟังก์ชั่นสาธารณะที่ใช้งานอยู่ (DateTimeImmutable $modifiedAt): เป็นโมฆะ{//ลดความซับซ้อน$this->status = Status::active();$this->modifiedAt = $modifiedAt; }ฟังก์ชันสาธารณะหมดอายุ (DateTimeImmutable $modifiedAt): void{//siplication$this->status = Status::expired();$this->modifiedAt = $modifiedAt; }ฟังก์ชันสาธารณะ isActive(): bool{return $this->status->isEqual(Status::active()); - -
คลาสสุดท้าย ValidTestExample ขยาย TestCase {/** * @test */ฟังก์ชันสาธารณะ renew_an_expired_subscription_is_possible(): void{$expiredSubscription = SubscriptionMother::expired();$sut = new ApplicationService($this->createRepository($expiredSubscription));$result = $sut- >renewSubscription(1);// ข้ามการตรวจสอบ modifiedAt เนื่องจากไม่ได้เป็นส่วนหนึ่งของพฤติกรรมที่สังเกตได้ ในการตรวจสอบค่านี้ เรา// จะต้องเพิ่ม getter สำหรับ modifiedAt ซึ่งอาจใช้เพื่อการทดสอบเท่านั้น self::assertTrue($expiredSubscription->isActive());self::assertTrue($result); self::assertTrue($result); }/** * @test */public ฟังก์ชั่น 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($ผลลัพธ์); } ฟังก์ชั่นส่วนตัว createRepository (การสมัครสมาชิก $ subscription): SubscriptionRepositoryInterface { คืนคลาสใหม่ ($ expiredSubscription) ใช้งาน SubscriptionRepositoryInterface { ฟังก์ชั่นสาธารณะ __ สร้าง (การสมัครสมาชิกแบบอ่านอย่างเดียวส่วนตัว $ subscription) {} ฟังก์ชั่นสาธารณะ findById (int $id): การสมัครสมาชิก {return $ this-> การสมัครสมาชิก; - - - -
บันทึก
รูปแบบการสมัครสมาชิกแรกมีการออกแบบที่ไม่ดี หากต้องการเรียกใช้การดำเนินธุรกิจ คุณต้องเรียกสามวิธี การใช้ getters เพื่อตรวจสอบการดำเนินการไม่ใช่แนวปฏิบัติที่ดี ในกรณีนี้ ระบบจะข้ามการตรวจสอบการเปลี่ยนแปลงของ modifiedAt
การตั้งค่าเฉพาะ modifiedAt
ในระหว่างการดำเนินการต่ออายุสามารถทดสอบได้กับการดำเนินธุรกิจที่หมดอายุ ไม่จำเป็นต้องใช้ getter สำหรับ modifiedAt
แน่นอนว่า มีบางกรณีที่การค้นหาความเป็นไปได้ที่จะหลีกเลี่ยง getters ที่มีไว้สำหรับการทดสอบเท่านั้นจะยากมาก แต่เราควรพยายามอย่าแนะนำพวกเขาเสมอ
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
คลาส CannotSuspendExpiredSubscriptionPolicy ใช้ SuspendingPolicyInterface { ฟังก์ชั่นสาธารณะระงับ (การสมัครสมาชิก $ การสมัครสมาชิก, DateTimeImmutable $ at): บูล { ถ้า ($ การสมัครสมาชิก -> isExpired ()) { คืนเท็จ; } คืนค่าจริง; - -
คลาส CannotSuspendExpiredSubscriptionPolicyTest ขยาย TestCase {/** * @test */ฟังก์ชันสาธารณะ it_returns_false_when_a_subscription_is_expired(): เป็นโมฆะ{$policy = ใหม่ CannotSuspendExpiredSubscriptionPolicy();$subscription = $this->createStub(สมัครสมาชิก::class);$subscription->method('isExpired')->willReturn(true);self::assertFalse($policy->suspend($subscription, new DateTimeImmutable())); }/** * @test */ฟังก์ชันสาธารณะ it_returns_true_when_a_subscription_is_not_expired(): เป็นโมฆะ{$policy = ใหม่ CannotSuspendExpiredSubscriptionPolicy();$subscription = $this->createStub(สมัครสมาชิก::class);$subscription->method('isExpired')->willReturn(false);self::assertTrue($policy->suspend($subscription, new DateTimeImmutable())); - -
คลาส CannotSuspendNewSubscriptionPolicy ใช้ SuspendingPolicyInterface { ฟังก์ชั่นสาธารณะระงับ (สมัครสมาชิก $ สมัครสมาชิก, DateTimeImmutable $ ที่): บูล {if ($ สมัครสมาชิก -> isNew ()) { กลับเท็จ; } คืนค่าจริง; - -
คลาส CannotSuspendNewSubscriptionPolicyTest ขยาย TestCase {/** * @test */ฟังก์ชั่นสาธารณะ it_returns_false_when_a_subscription_is_new(): void{$policy = new CannotSuspendNewSubscriptionPolicy();$subscription = $this->createStub(สมัครสมาชิก::class);$subscription->method('isNew')->willReturn(true);self::assertFalse($policy->suspend($subscription, new DateTimeImmutable())); }/** * @test */ฟังก์ชันสาธารณะ it_returns_true_when_a_subscription_is_not_new(): void{$policy = new CannotSuspendNewSubscriptionPolicy();$subscription = $this->createStub(สมัครสมาชิก::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 */ฟังก์ชั่นสาธารณะ it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable('2021-01-29');$policy = new CanSuspendAfterOneMonthPolicy();$subscription = new Subscription(ใหม่ 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(ใหม่ 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 = 'suspended';ฟังก์ชันส่วนตัว __construct (สตริง $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); } ฟังก์ชั่นสาธารณะ isEqual (สถานะ $ ของตัวเอง): 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)); - -
class SubscriptionTest ขยาย TestCase {/** * @test */public function suspension_a_subscription_is_possible_when_a_policy_returns_true(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'ระงับ')->willReturn(true);$sut = การสมัครสมาชิกใหม่ (new DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertTrue($result);self::assertTrue($sut->isSuspended()); }/** * @test */public ฟังก์ชั่น suspension_a_subscription_is_not_possible_when_a_policy_returns_false(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'ระงับ')->willReturn(false);$sut = new การสมัครสมาชิก(ใหม่ DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertFalse($result);self::assertFalse($sut->isSuspended()); }/** * @test */public function it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1M'));$sut = ใหม่ การสมัครสมาชิก($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 = ใหม่ การสมัครสมาชิก($date);self::assertTrue($sut->isOlderThan($futureDate)); - -
[!ATTENTION] ห้ามเขียนโค้ด 1:1, 1 คลาส : 1 ทดสอบ มันนำไปสู่การทดสอบที่เปราะบางซึ่งทำให้การปรับโครงสร้างใหม่เป็นเรื่องยาก
[!TIP|style:flat|ป้ายกำกับ:GOOD]
คลาสสุดท้าย CannotSuspendExpiredSubscriptionPolicy ใช้ SuspendingPolicyInterface { ฟังก์ชั่นสาธารณะระงับ (การสมัครสมาชิก $ การสมัครสมาชิก, DateTimeImmutable $ at): บูล { ถ้า ($ การสมัครสมาชิก -> isExpired ()) { คืนเท็จ; } คืนค่าจริง; - -
คลาสสุดท้าย CannotSuspendNewSubscriptionPolicy ใช้ SuspendingPolicyInterface { ฟังก์ชั่นสาธารณะระงับ (สมัครสมาชิก $ สมัครสมาชิก, DateTimeImmutable $ ที่): บูล {if ($ สมัครสมาชิก -> isNew ()) { กลับเท็จ; } คืนค่าจริง; - -
คลาสสุดท้าย 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 = 'suspended';ฟังก์ชันส่วนตัว __construct (สตริง $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); } ฟังก์ชั่นสาธารณะ isEqual (สถานะ $ ของตัวเอง): bool {return $this->status === $status->status; - -
การสมัครสมาชิกคลาสสุดท้าย {สถานะส่วนตัว $status;DateTimeImmutable $createdAt ส่วนตัว; ฟังก์ชั่นสาธารณะ __construct (DateTimeImmutable $createdAt) {$this->status = สถานะ::new();$this->createdAt = $createdAt; } ฟังก์ชั่นสาธารณะระงับ (SuspendingPolicyInterface $suspendingPolicy, DateTimeImmutable $at): bool{$result = $suspendingPolicy->suspend($this, $at);if ($result) {$this->status = Status::suspended() ; } ส่งคืนผลลัพธ์ $; } ฟังก์ชั่นสาธารณะ isOlderThan (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()); }ฟังก์ชั่นสาธารณะ isSuspended(): bool{return $this->status->isEqual(Status::suspended()); - -
SubscriptionSuspendingTest คลาสสุดท้ายขยาย TestCase {/** * @test */public ฟังก์ชั่น suspension_an_expired_subscription_with_cannot_suspend_expired_policy_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$sut->expire();$result = $sut -> ระงับ (ใหม่ ไม่สามารถระงับ ExpiredSubscriptionPolicy(), DateTimeImmutable() ใหม่);self::assertFalse($result); }/** * @test */public function suspension_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible(): void{$sut = new Subscription(new DateTimeImmutable());$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy(), ใหม่ DateTimeImmutable());ตนเอง::assertFalse($ผลลัพธ์); }/** * @test */public ฟังก์ชั่น suspension_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void{$sut = new Subscription(new DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy() , ใหม่ DateTimeImmutable());ตนเอง::assertTrue($ผลลัพธ์); }/** * @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());ตนเอง::assertTrue($ผลลัพธ์); }/** * @test */public function suspension_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($ผลลัพธ์); }/** * @test */public function suspension_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($ผลลัพธ์); - -
จะ Unit Test คลาสแบบนี้ได้อย่างไร?
คลาส ApplicationService { ฟังก์ชั่นสาธารณะ __construct (ส่วนตัวอ่านอย่างเดียว OrderRepository $orderRepository, ส่วนตัว อ่านอย่างเดียว FormRepository $formRepository) {} ฟังก์ชั่นสาธารณะ changeFormStatus(int $orderId): โมฆะ{$order = $this->orderRepository->getById($orderId);$soapResponse = $this->getSoapClient()->getStatusByOrderId($orderId);$form = $this->formRepository->getByOrderId($orderId);$form->setStatus($soapResponse['status']);$form-> setModifiedAt (DateTimeImmutable ใหม่ ()); ถ้า ($soapResponse ['สถานะ'] === 'ยอมรับ') {$order->setStatus('จ่ายแล้ว'); }$this->formRepository->save($form);$this->orderRepository->save($order); } ฟังก์ชั่นส่วนตัว getSoapClient (): SoapClient {คืน SoapClient ใหม่ ('https://legacy_system.pl/Soap/WebService', []); - -
[!TIP|style:flat|ป้ายกำกับ:GOOD]
จำเป็นต้องแยกโค้ดที่ซับซ้อนเกินไปเพื่อแยกคลาส
ApplicationService คลาสสุดท้าย {ฟังก์ชั่นสาธารณะ __ สร้าง (ส่วนตัวอ่านอย่างเดียว OrderRepositoryInterface $orderRepository, ส่วนตัวอ่านอย่างเดียว FormRepositoryInterface $formRepository, ส่วนตัวอ่านอย่างเดียว FormApiInterface $formApi, ส่วนตัวอ่านอย่างเดียว ChangeFormStatusService $changeFormStatusService) {} ฟังก์ชั่นสาธารณะ changeFormStatus(int $orderId): โมฆะ{$order = $this->orderRepository->getById($orderId);$form = $this->formRepository->getByOrderId($orderId);$status = $this->formApi->getStatusByOrderId($orderId);$this->changeFormStatusService ->changeStatus($คำสั่งซื้อ, $แบบฟอร์ม, $status);$this->formRepository->save($form);$this->orderRepository->save($order); - -
ChangeFormStatusService คลาสสุดท้าย {ฟังก์ชันสาธารณะ changeStatus(สั่งซื้อ $order, แบบฟอร์ม $form, สตริง $formStatus): void{$status = FormStatus::createFromString($formStatus);$form->changeStatus($status);if ($form->isAccepted( )) {$order->changeStatus(OrderStatus::paid()); - - -
คลาสสุดท้าย ChangingFormStatusTest ขยาย TestCase {/** * @test */public function changes_a_form_status_to_accepted_changes_an_order_status_to_paid(): void{$order = new Order();$form = new Form();$status = 'accepted';$sut = new ChangeFormStatusService();$sut ->changeStatus($คำสั่งซื้อ, $แบบฟอร์ม, $status);self::assertTrue($form->isAccepted());self::assertTrue($order->isPaid()); }/** * @test */public function changes_a_form_status_to_refused_not_changes_an_order_status(): void{$order = new Order();$form = new Form();$status = 'new';$sut = new ChangeFormStatusService();$sut ->changeStatus($คำสั่งซื้อ, $แบบฟอร์ม, $status);self::assertFalse($form->isAccepted());self::assertFalse($order->isPaid()); - -
อย่างไรก็ตาม ApplicationService น่าจะได้รับการทดสอบโดยการทดสอบการรวมกับ FormApiInterface ที่จำลองเท่านั้น
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
ลูกค้าชั้นสุดท้าย { ฟังก์ชั่นสาธารณะ __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()); - -
EventSubscriber คลาสสุดท้าย {ฟังก์ชันคงที่สาธารณะ getSubscribedEvents(): array{return ['event' => 'onEvent']; }ฟังก์ชั่นสาธารณะ onEvent(): เป็นโมฆะ{ - -
คลาสสุดท้าย EventSubscriberTest ขยาย TestCase {ฟังก์ชันสาธารณะ testGetSubscribedEvents(): void{$result = EventSubscriber::getSubscribedEvents();self::assertSame(['event' => 'onEvent'], $result); - -
[!ความสนใจ] การทดสอบโค้ดโดยไม่มีตรรกะที่ซับซ้อนนั้นไม่สมเหตุสมผล แต่ก็นำไปสู่การทดสอบที่เปราะบางเช่นกัน
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
UserRepository คลาสสุดท้าย { ฟังก์ชั่นสาธารณะ __construct (การเชื่อมต่อ $connection แบบอ่านอย่างเดียวส่วนตัว) {} ฟังก์ชั่นสาธารณะ getUserNameByEmail (สตริง $ อีเมล): ?array{return $this->connection->createQueryBuilder() ->จาก('ผู้ใช้', 'u') ->where('u.email = :email') ->setParameter('อีเมล', $อีเมล) ->ดำเนินการ() ->ดึงข้อมูล(); - -
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->expects($this->once()) ->method('createQueryBuilder') ->willReturn($queryBuilder);$queryBuilder->คาดหวัง($this->once()) -> วิธีการ ('จาก') ->with('ผู้ใช้', 'u') ->willReturn($queryBuilder);$queryBuilder->คาดหวัง($this->once()) -> วิธีการ ('ที่ไหน') ->with('u.email = :email') ->willReturn($queryBuilder);$queryBuilder->คาดหวัง($this->once()) ->วิธีการ('setParameter') ->with('อีเมล', $อีเมล) ->willReturn($queryBuilder);$queryBuilder->คาดหวัง($this->once()) ->วิธีการ('ดำเนินการ') -> willreturn ($ result); $ result-> คาดว่า ($ this-> ครั้งเดียว ()) -> วิธีการ ('Fetch') -> willreturn (['อีเมล' => $ อีเมล]); $ result = $ userrepository-> getUserNameByEmail ($ อีเมล); Self :: assertSame (['อีเมล' => $ อีเมล], $ ผลลัพธ์); - -
[!ความสนใจ] การทดสอบที่เก็บข้อมูลในลักษณะดังกล่าวนำไปสู่การทดสอบที่เปราะบาง จากนั้นการปรับโครงสร้างใหม่ก็ทำได้ยาก เพื่อทดสอบที่เก็บข้อมูลให้เขียนการทดสอบการรวม
[!TIP|style:flat|ป้ายกำกับ:GOOD]
ชั้นสุดท้าย GoodTest ขยาย TestCase {Private SubscriptionFactory $ SUT; การตั้งค่าฟังก์ชั่นสาธารณะ (): เป็นโมฆะ {$ this-> sut = new SubscriptionFactory (); }/** * @test */ฟังก์ชั่นสาธารณะ creates_a_subscription_for_a_given_date_range (): เป็นโมฆะ {$ result = $ this-> sut-> สร้าง (การสมัครสมาชิก :: คลาส, $ ผลลัพธ์); }/** * @test */ฟังก์ชั่นสาธารณะ throws_an_exception_on_invalid_date_range (): เป็นโมฆะ {$ this-> คาดหวัง (createsubscriptionException :: คลาส); $ result = $ this-> sut-> create (dateTimeimmutable ใหม่ ('ตอนนี้ -1 ปี'), ใหม่ datetimeimmutable ()); - -
บันทึก
กรณีที่ดีที่สุดสำหรับการใช้วิธีการตั้งค่าคือการทดสอบอ็อบเจ็กต์ไร้สถานะ
การกำหนดค่าใดๆ ที่ทำภายใน setUp
จะทำการทดสอบร่วมกัน และมีผลกระทบต่อการทดสอบทั้งหมด
เป็นการดีกว่าที่จะหลีกเลี่ยงสถานะที่ใช้ร่วมกันระหว่างการทดสอบและกำหนดค่าสถานะเริ่มต้นตามวิธีการทดสอบ
ความสามารถในการอ่านแย่กว่าเมื่อเปรียบเทียบกับการกำหนดค่าที่ทำในวิธีทดสอบที่เหมาะสม
[!TIP|style:flat|ป้ายกำกับ:ดีกว่า]
ชั้นสุดท้าย BetterTest ขยาย TestCase {/** * @Test */ฟังก์ชั่นสาธารณะ suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible (): เป็นโมฆะ {$ sut = $ this-> createAnactiveScription (); : assertTrue ($ ผลลัพธ์); }/** * @Test */ฟังก์ชั่นสาธารณะ suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible (): โมฆะ {$ sut = $ this-> createAnactiveScription () dateTimeimmutable ()); self :: assertTrue ($ result); }/** * @Test */ฟังก์ชั่นสาธารณะ suspending_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible (): โมฆะ {$ sut = $ this-> createAnewSubscription (); $ result = $ sut-> : AssertFalse ($ ผลลัพธ์); } ฟังก์ชั่นส่วนตัว createAnewSubscription (): การสมัครสมาชิก {ส่งคืนการสมัครสมาชิกใหม่ (ใหม่ dateTimeImmutable ()); } ฟังก์ชั่นส่วนตัว createAnactiveSubscription (): การสมัครสมาชิก {$ subscription = การสมัครสมาชิกใหม่ (ใหม่ dateTimeImmutable ()); $ การสมัครสมาชิก-> activate (); ส่งคืนการสมัครสมาชิก $; - -
บันทึก
วิธีการนี้ช่วยเพิ่มความสามารถในการอ่านและชี้แจงการแยก (โค้ดอ่านมากกว่าเขียน)
ผู้ช่วยส่วนตัวอาจน่าเบื่อที่จะใช้ในวิธีทดสอบแต่ละวิธี แม้ว่าจะมีเจตนาชัดเจนก็ตาม
หากต้องการแชร์ออบเจ็กต์การทดสอบที่คล้ายกันระหว่างคลาสการทดสอบหลายคลาส ให้ใช้:
แม่วัตถุ
ช่างก่อสร้าง
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
ลูกค้าชั้นสุดท้าย {ส่วนตัวลูกค้าประเภท $ ประเภท; ส่วนลดส่วนตัวการคำนวณเชิงนโยบาย PolicyInterface $ discountCalculationPolicy; ฟังก์ชั่นสาธารณะ __Construct () {$ this-> type = customerType :: normal (); $ this-> discountCalculationPolicy = ใหม่ NormalDiscountPolicy (); } ฟังก์ชั่นสาธารณะ makeVip (): เป็นโมฆะ {$ this-> type = customerType :: vip (); $ this-> ส่วนลดการคำนวณเชิงนโยบาย = ใหม่ vipdiscountPolicy (); } ฟังก์ชั่นสาธารณะ getCustomerType (): customerType {return $ this-> type; } ฟังก์ชั่นสาธารณะ getPerCentAdeIscount (): int {return $ this-> discountCalculationPolicy-> getPercentAgediscount (); - -
คลาสสุดท้าย INVALIDTEST ขยาย TESTCASE {ฟังก์ชั่นสาธารณะ testmakeVip (): เป็นโมฆะ {$ sut = ลูกค้าใหม่ (); $ sut-> makevip (); self :: assertsame (customerType :: vip (), $ sut-> getCustomerType ()); - -
[!TIP|style:flat|ป้ายกำกับ:GOOD]
ลูกค้าชั้นสุดท้าย {ส่วนตัวลูกค้าประเภท $ ประเภท; ส่วนลดส่วนตัวการคำนวณเชิงนโยบาย PolicyInterface $ discountCalculationPolicy; ฟังก์ชั่นสาธารณะ __Construct () {$ this-> type = customerType :: normal (); $ this-> discountCalculationPolicy = ใหม่ NormalDiscountPolicy (); } ฟังก์ชั่นสาธารณะ makeVip (): เป็นโมฆะ {$ this-> type = customerType :: vip (); $ this-> ส่วนลดการคำนวณเชิงนโยบาย = ใหม่ vipdiscountPolicy (); } ฟังก์ชั่นสาธารณะ getPerCentAdeIscount (): int {return $ this-> discountCalculationPolicy-> getPercentAgediscount (); - -
Validtest ชั้นสุดท้ายขยาย TestCase {/** * @Test */ฟังก์ชั่นสาธารณะ A_VIP_CUSTOMER_HAS_A_25_PERCENTAGE_DISCOUNT (): เป็นโมฆะ {$ SUT = ลูกค้าใหม่ (); $ SUT-> MAKEVIP (); Self :: assertSame (25, $ SUT-> - -
[!ความสนใจ] การเพิ่มรหัสการผลิตเพิ่มเติม (เช่น getter getCustomerType()) เพียงเพื่อตรวจสอบสถานะในการทดสอบถือเป็นแนวปฏิบัติที่ไม่ดี ควรได้รับการตรวจสอบโดยค่านัยสำคัญของโดเมนอื่น (ในกรณีนี้คือ getPercentageDiscount()) แน่นอนว่าบางครั้งอาจเป็นเรื่องยากที่จะหาวิธีอื่นในการตรวจสอบการทำงาน และเราอาจถูกบังคับให้เพิ่มรหัสการผลิตเพิ่มเติมเพื่อตรวจสอบความถูกต้องในการทดสอบ แต่เราควรพยายามหลีกเลี่ยงสิ่งนั้น
ส่วนลดคลาสสุดท้าย {ฟังก์ชั่นสาธารณะคำนวณ (int $ isvipfromyears): int { ยืนยัน :: Greaterthaneq ($ isvipfromyears, 0); return min (($ isvipfromyears * 10) + 3, 80); - -
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
คลาสสุดท้าย INVALIDTEST ขยาย TESTCASE {/** * @dataprovider DuckingDataprovider */ฟังก์ชั่นสาธารณะ testCalculate (int $ vipdaysfrom, int $ คาดหวัง): void {$ sut = new DiscountCalculator (); Self :: assertSame ($ คาดหวัง ); } ฟังก์ชั่นสาธารณะ DiscountDataprovider (): อาร์เรย์ {return [ [0, 0 * 10 + 3], // รายละเอียดโดเมนรั่ว [1, 1 * 10 + 3] [5, 5 * 10 + 3], [8, 80] - - -
[!TIP|style:flat|ป้ายกำกับ:GOOD]
Validtest ชั้นสุดท้ายขยาย TestCase {/** * @dataprovider DuckingDataprovider */ฟังก์ชั่นสาธารณะ testCalculate (int $ vipdaysfrom, int $ คาดหวัง): void {$ sut = new DiscountCalculator (); Self :: assertSame ($ คาดหวัง ); } ฟังก์ชั่นสาธารณะ DiscountDataprovider (): อาร์เรย์ {return [ [0, 3], [1, 13], [5, 53], [8, 80] - - -
บันทึก
อย่าทำซ้ำตรรกะการผลิตในการทดสอบ เพียงตรวจสอบผลลัพธ์ด้วยค่าฮาร์ดโค้ด
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
คลาสส่วนลด calcalculator {ฟังก์ชั่นสาธารณะ calculateinternaldiscount (int $ isvipfromyears): int { ยืนยัน :: Greaterthaneq ($ isvipfromyears, 0); return min (($ isvipfromyears * 10) + 3, 80); } ฟังก์ชั่นสาธารณะ calculateadditionaldiscountFromexternalsystem (): int {// รับข้อมูลจากระบบภายนอกเพื่อคำนวณการตัดสิน 5; - -
คำสั่งชั้นเรียน {ฟังก์ชั่นสาธารณะ __Construct (ส่วนตัวส่วนลดส่วนตัวส่วนลด $ discountCalculator) {} ฟังก์ชั่นสาธารณะ getTotalPriceWithDiscount (int $ totalPrice, int $ vipfromdays): int {$ internaldiscount = $ this-> ส่วนลด $ this-> discountcalculator-> calculateadditionaldiscountFromexternalsystem (); $ discountsum = $ internaldiscount + $ externaldiscount; return $ totalprice-(int) ceil (($ totalprice * $ discountsum) / 100); - -
คลาสสุดท้าย INVALIDTEST ขยาย TESTCASE {/** * @dataprovider orderdataprovider */ฟังก์ชั่นสาธารณะ testgetTotalpriceWithDiscount (int $ totalprice, int $ vipdaysfrom, int $ คาดหวัง): เป็นโมฆะ ['calculateadditionaldiscountFromexternalsystem']); $ discountCalculator-> วิธีการ ('calculateadditionaldiscountFromexternalsystem')-> Willreturn (5); $ sut = คำสั่งใหม่ , $ vipdaysfrom)); } ฟังก์ชั่นสาธารณะ orderdataprovider (): อาร์เรย์ {return [ [1,000, 0, 920], [500, 1, 410], [644, 5, 270], - - -
[!TIP|style:flat|ป้ายกำกับ:GOOD]
อินเตอร์เฟส externaldiscountCalculatorInterface {ฟังก์ชั่นสาธารณะคำนวณ (): int; -
คลาสสุดท้าย InternaldiscountCalculator {ฟังก์ชั่นสาธารณะคำนวณ (int $ isvipfromyears): int { ยืนยัน :: Greaterthaneq ($ isvipfromyears, 0); return min (($ isvipfromyears * 10) + 3, 80); - -
คำสั่งชั้นเรียนสุดท้าย {ฟังก์ชั่นสาธารณะ __Construct (ส่วนตัวอ่านอย่างเดียว InternaldIscountCalculator $ ส่วนลด calculator ส่วนตัว readonly externaldiscountCalculatorInterface $ externaldiscountCalculator) {} ฟังก์ชั่นสาธารณะ getTotalPriceWithDiscount (int $ totalPrice, int $ vipfromdays): $ externeraldiscount = $ this-> externaldiscountCalculator-> คำนวณ (); $ discountsum = $ internaldiscount + $ externaldiscount; return $ totalPrice-(int) ceil (($ totalPrice * $ discountsum) / 100); - -
Validtest ชั้นสุดท้ายขยาย TestCase {/** * @dataprovider OrderDataprovider */ฟังก์ชั่นสาธารณะ TestGetTotAlpriceWithDiscount (int $ TotalPrice, int $ vipdaysFrom, int $ คาดหวัง): void {$ externaldiscountCalculator = ใหม่คลาส () - }; $ sut = คำสั่งใหม่ (ใหม่ InternaldiscountCalculator (), $ externaldiscountCalculator); Self :: AssertSame ($ คาดหวัง, $ sut-> getTotalPriceWithDiscount ($ TotalPrice, $ VIPDAYSFROM)); } ฟังก์ชั่นสาธารณะ orderdataprovider (): อาร์เรย์ {return [ [1,000, 0, 920], [500, 1, 410], [644, 5, 270], - - -
บันทึก
ความจำเป็นในการเยาะเย้ยชั้นเรียนที่เป็นรูปธรรมเพื่อแทนที่ส่วนหนึ่งของพฤติกรรมหมายความว่าชั้นเรียนนี้อาจซับซ้อนเกินไปและละเมิดหลักการความรับผิดชอบเดียว
OrderItem ชั้นเรียนสุดท้าย {ฟังก์ชั่นสาธารณะ __Construct (สาธารณะ readingly int $ ทั้งหมด) {} -
ลำดับชั้นสุดท้าย {/** * @param orderitem [] $ items * @param int $ transportcost */ฟังก์ชั่นสาธารณะ __Construct (อาร์เรย์ส่วนตัว $ รายการ, รายการส่วนตัว $ transportCost) {} ฟังก์ชั่นสาธารณะ getTotal (): int {return $ this-> getItemStotal () + $ this-> transportcost; } ฟังก์ชั่นส่วนตัว getItemStotal (): int {return array_reduce (array_map (fn (orderitem $ item) => $ item-> ทั้งหมด, $ this-> รายการ), fn (int $ sum, int $ ทั้งหมด) => $ sum + = $ ทั้งหมด, 0); - -
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
คลาสสุดท้าย INVALIDTEST ขยาย TESTCASE {/** * @test * @dataprovider ordersdataprovider */ฟังก์ชั่นสาธารณะ get_total_returns_a_total_cost_of_a_whole_order (สั่งซื้อ $, int $ คาดหวัง): void {self :: assertsame }/** * @test * @dataprovider orderitemsdataprovider */ฟังก์ชั่นสาธารณะ get_items_total_returns_a_total_cost_of_all_items (ลำดับ $, int $ คาดหวัง } ฟังก์ชั่นสาธารณะ ordersdataprovider (): อาร์เรย์ {return [ [คำสั่งซื้อใหม่ ([ใหม่ OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 75], [คำสั่งซื้อใหม่ ([ใหม่ OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [คำสั่งซื้อใหม่ ([ใหม่ OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 306] - } ฟังก์ชั่นสาธารณะ OrderItemsDatapRovider (): อาร์เรย์ {return [ [คำสั่งซื้อใหม่ ([ใหม่ OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 60], [คำสั่งซื้อใหม่ ([ใหม่ OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [คำสั่งซื้อใหม่ ([ใหม่ OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 297] - } ฟังก์ชั่นส่วนตัว invokeprivateMethodGetItemStotal (คำสั่งซื้อ & $ สั่ง): int {$ reflection = ใหม่ reflectionClass (get_class ($ order)); $ method = $ reflection-> getMethod ('getItemStotal'); $ method-> setAccessible $ method-> invokeargs ($ order, []); - -
[!TIP|style:flat|ป้ายกำกับ:GOOD]
Validtest ชั้นสุดท้ายขยาย TestCase {/** * @test * @dataprovider ordersdataprovider */ฟังก์ชั่นสาธารณะ get_total_returns_a_total_cost_of_a_whole_order (สั่งซื้อ $, int $ คาดหวัง): void {self :: assertsame } ฟังก์ชั่นสาธารณะ ordersdataprovider (): อาร์เรย์ {return [ [คำสั่งซื้อใหม่ ([ใหม่ OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 75], [คำสั่งซื้อใหม่ ([ใหม่ OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [คำสั่งซื้อใหม่ ([ใหม่ OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 306] - - -
[!ความสนใจ] การทดสอบควรตรวจสอบ API สาธารณะเท่านั้น
เวลาเป็นสิ่งที่ขึ้นอยู่กับความผันผวนเนื่องจากไม่สามารถกำหนดได้ การร้องขอแต่ละครั้งจะส่งกลับผลลัพธ์ที่แตกต่างกัน
[!คำเตือน|style:flat|ป้ายกำกับ:BAD]
นาฬิกาชั้นเรียนสุดท้าย {Public Static DateTime | null $ currentDateTime = null; ฟังก์ชั่นคงที่สาธารณะ getCurrentDateTime (): DateTime {ถ้า (null === self :: $ currentDateTime) {self :: $ currentDateTime = new DateTime (); } return self :: $ currentdatetime; } ชุดฟังก์ชั่นคงที่สาธารณะ (DateTime $ datetime): เป็นโมฆะ {self :: $ currentDateTime = $ datetime; } ฟังก์ชั่นคงที่สาธารณะรีเซ็ต (): โมฆะ {self :: $ currentDateTime = null; - -
ลูกค้าชั้นสุดท้าย {Private DateTime $ CreateDat; ฟังก์ชั่นสาธารณะ __Construct () {$ this-> createDat = นาฬิกา :: getCurrentDateTime (); } ฟังก์ชั่นสาธารณะ isvip (): bool {return $ this-> createdat-> diff (นาฬิกา :: getCurrentDateTime ())-> y> = 1; - -
คลาสสุดท้าย INVALIDTEST ขยาย TESTCASE {/** * @Test */ฟังก์ชั่นสาธารณะ A_CUSTOMER_REGISTERED_MORE_THAN_A_ONE_YEAR_AGO_IS_A_VIP (): เป็นโมฆะ { นาฬิกา :: set (dateTime ใหม่ ('2019-01-01')); $ sut = ลูกค้าใหม่ (); นาฬิกา :: รีเซ็ต (); // คุณต้องจำไว้เกี่ยวกับการรีเซ็ตสถานะที่ใช้ร่วมกัน :: assertTrue ($ sut-> isvip ()); }/** * @Test */ฟังก์ชั่นสาธารณะ A_CUSTOMER_REGISTERED_LESS_THAN_A_ONE_YEAR_AGO_IS_NOT_A_VIP (): โมฆะ { นาฬิกา :: set ((dateTime ใหม่ ())-> sub (ใหม่ dateInterval ('p2m'))); $ sut = ลูกค้าใหม่ (); นาฬิกา :: รีเซ็ต (); // คุณต้องจำไว้เกี่ยวกับการรีเซ็ต stateelf ที่ใช้ร่วมกัน :: assertFalse ($ sut-> isvip ()); - -
[!TIP|style:flat|ป้ายกำกับ:GOOD]
อินเทอร์เฟซ ClockInterface {ฟังก์ชั่นสาธารณะ getCurrentTime (): dateTimeimmutable; -
นาฬิกาคลาสสุดท้ายใช้ clockinterface {ฟังก์ชั่นส่วนตัว __Construct () - } ฟังก์ชั่นคงที่สาธารณะสร้าง (): ตัวเอง {return new self (); } ฟังก์ชั่นสาธารณะ getCurrentTime (): dateTimeimmutable {ส่งคืน dateTimeimmutable ใหม่ (); - -
คลาสสุดท้าย ExidiveClock ใช้งาน ClockInterface {ฟังก์ชั่นส่วนตัว __Construct (ส่วนตัว readonly datetimeimmutable $ recimeDate) {} ฟังก์ชั่นคงที่สาธารณะสร้าง (datetimeimmutable $ reamixdate): self {return self ใหม่ ($ reamiveDate); } ฟังก์ชั่นสาธารณะ getCurrentTime (): dateTimeimmutable {return $ this-> recideDate; - -
ลูกค้าชั้นสุดท้าย {ฟังก์ชั่นสาธารณะ __Construct (ส่วนตัว readonly dateTimeimmutable $ createDat) {} ฟังก์ชั่นสาธารณะ ISVIP (datetimeimmutable $ currentDate): bool {return $ this-> createdat-> diff ($ currentDate)-> y> = 1; - -
Validtest ชั้นสุดท้ายขยาย TestCase {/** * @Test */ฟังก์ชั่นสาธารณะ A_CUSTOMER_REGISTERED_MORE_THAN_A_ONE_YEAR_AGO_IS_A_VIP (): เป็นโมฆะ {$ SUT = ลูกค้าใหม่ assertTrue ($ sut-> isvip (recidiveclock :: create (ใหม่ DateTimeimmutable ('2020-01-02'))-> getCurrentTime ())); }/** * @Test */ฟังก์ชั่นสาธารณะ A_CUSTOMER_REGISTERED_LESS_THAN_A_ONE_YEAR_AGO_IS_NOT_A_VIP (): เป็นโมฆะ {$ SUT = ลูกค้าใหม่ DateTimeImmutable ('2019-01-01'))-> getCurrentTime ()); self :: assertFalse ($ sut-> isvip (recidectclock :: create (datetimeimmutable ใหม่ ('2019-05-02' ))); - -
บันทึก
ไม่ควรสร้างเวลาและตัวเลขสุ่มในรหัสโดเมนโดยตรง ในการทดสอบพฤติกรรม เราต้องมีผลลัพธ์ที่กำหนด ดังนั้นเราจำเป็นต้องแทรกค่าเหล่านี้ลงในอ็อบเจ็กต์โดเมนเหมือนในตัวอย่างด้านบน
ความครอบคลุม 100% ไม่ใช่เป้าหมายหรือเป็นสิ่งที่ไม่พึงประสงค์ด้วยซ้ำ เพราะหากมีการครอบคลุม 100% การทดสอบอาจจะเปราะบางมาก ซึ่งหมายความว่าการปรับโครงสร้างใหม่จะยากมาก การทดสอบการกลายพันธุ์ให้ผลตอบรับที่ดีขึ้นเกี่ยวกับคุณภาพของการทดสอบ อ่านเพิ่มเติม
การพัฒนาแบบทดสอบขับเคลื่อน: ตัวอย่าง / Kent Beck - คลาสสิก
หลักการ การปฏิบัติ และรูปแบบการทดสอบหน่วย / Vladimir Khorikov - หนังสือเกี่ยวกับการทดสอบที่ดีที่สุดที่ฉันเคยอ่าน
คามิล รุคซินสกี้
ทวิตเตอร์: https://twitter.com/Sarvendev
บล็อก: https://sarvendev.com/
LinkedIn: https://www.linkedin.com/in/kamilruczynski/