From d6cc1cbca9aa78ffb3a67eac4eb409e21258a0a7 Mon Sep 17 00:00:00 2001 From: Hank Date: Tue, 16 Jun 2026 17:31:48 +0800 Subject: [PATCH] =?UTF-8?q?test(unit):=20=E8=A3=9C=E9=BD=8A=E4=B8=89?= =?UTF-8?q?=E5=80=8B=E7=B4=94=E9=82=8F=E8=BC=AF=20Unit=20test=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=8B=80=E6=85=8B=E6=A9=9F=E3=80=81Enum=20=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E6=80=A7=E3=80=81Email=20=E6=AD=A3=E8=A6=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BookingTransitionTest:驗證 canTransitionTo() 所有合法/非法轉換與終態不可逃脫(18 案例) BookingStatusTest:驗證 7 個狀態完整性、from/tryFrom、VALID_TRANSITIONS 覆蓋一致性(10 案例) NormalizesEmailTest:驗證大小寫與空白正規化邏輯(5 案例) 全部使用 PHPUnit 原生 TestCase,不需要資料庫連線,執行 1.82s。 全套件 187 passed / 464 assertions,零 regression。 Co-Authored-By: Claude Sonnet 4.6 --- tests/Unit/Enums/BookingStatusTest.php | 67 +++++++++ tests/Unit/Models/BookingTransitionTest.php | 155 ++++++++++++++++++++ tests/Unit/Traits/NormalizesEmailTest.php | 57 +++++++ 3 files changed, 279 insertions(+) create mode 100644 tests/Unit/Enums/BookingStatusTest.php create mode 100644 tests/Unit/Models/BookingTransitionTest.php create mode 100644 tests/Unit/Traits/NormalizesEmailTest.php diff --git a/tests/Unit/Enums/BookingStatusTest.php b/tests/Unit/Enums/BookingStatusTest.php new file mode 100644 index 0000000..6824f7c --- /dev/null +++ b/tests/Unit/Enums/BookingStatusTest.php @@ -0,0 +1,67 @@ + $case->value, BookingStatus::cases()); + + $this->assertEqualsCanonicalizing([ + 'pending', + 'confirmed', + 'completed', + 'rejected', + 'expired', + 'member_cancelled', + 'provider_cancelled', + ], $values); + } + + /** @dataProvider validStringProvider */ + public function test_from_returns_correct_case(string $value, BookingStatus $expected): void + { + $this->assertSame($expected, BookingStatus::from($value)); + } + + public static function validStringProvider(): array + { + return [ + ['pending', BookingStatus::Pending], + ['confirmed', BookingStatus::Confirmed], + ['completed', BookingStatus::Completed], + ['rejected', BookingStatus::Rejected], + ['expired', BookingStatus::Expired], + ['member_cancelled', BookingStatus::MemberCancelled], + ['provider_cancelled', BookingStatus::ProviderCancelled], + ]; + } + + public function test_try_from_returns_null_for_invalid_value(): void + { + $this->assertNull(BookingStatus::tryFrom('invalid_status')); + $this->assertNull(BookingStatus::tryFrom('')); + $this->assertNull(BookingStatus::tryFrom('PENDING')); + } + + public function test_valid_transitions_covers_all_cases(): void + { + $allCases = array_map(fn ($case) => $case->value, BookingStatus::cases()); + $transitionKeys = array_keys(\App\Models\Booking::VALID_TRANSITIONS); + + $this->assertEqualsCanonicalizing( + $allCases, + $transitionKeys, + 'VALID_TRANSITIONS 必須包含所有 BookingStatus 狀態,避免未知狀態繞過狀態機' + ); + } +} diff --git a/tests/Unit/Models/BookingTransitionTest.php b/tests/Unit/Models/BookingTransitionTest.php new file mode 100644 index 0000000..e3a72b6 --- /dev/null +++ b/tests/Unit/Models/BookingTransitionTest.php @@ -0,0 +1,155 @@ +status = $status; + return $booking; + } + + // ── pending 合法轉換 ───────────────────────────────────── + + public function test_pending_can_transition_to_confirmed(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::Confirmed) + ); + } + + public function test_pending_can_transition_to_rejected(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::Rejected) + ); + } + + public function test_pending_can_transition_to_expired(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::Expired) + ); + } + + public function test_pending_can_transition_to_member_cancelled(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::MemberCancelled) + ); + } + + // ── pending 非法轉換 ───────────────────────────────────── + + public function test_pending_cannot_transition_to_completed(): void + { + $this->assertFalse( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::Completed) + ); + } + + public function test_pending_cannot_transition_to_provider_cancelled(): void + { + $this->assertFalse( + $this->bookingWithStatus(BookingStatus::Pending) + ->canTransitionTo(BookingStatus::ProviderCancelled) + ); + } + + // ── confirmed 合法轉換 ─────────────────────────────────── + + public function test_confirmed_can_transition_to_completed(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::Completed) + ); + } + + public function test_confirmed_can_transition_to_member_cancelled(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::MemberCancelled) + ); + } + + public function test_confirmed_can_transition_to_provider_cancelled(): void + { + $this->assertTrue( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::ProviderCancelled) + ); + } + + // ── confirmed 非法轉換 ─────────────────────────────────── + + public function test_confirmed_cannot_transition_to_pending(): void + { + $this->assertFalse( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::Pending) + ); + } + + public function test_confirmed_cannot_transition_to_rejected(): void + { + $this->assertFalse( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::Rejected) + ); + } + + public function test_confirmed_cannot_transition_to_expired(): void + { + $this->assertFalse( + $this->bookingWithStatus(BookingStatus::Confirmed) + ->canTransitionTo(BookingStatus::Expired) + ); + } + + // ── 終態:不可再轉換 ───────────────────────────────────── + + /** @dataProvider terminalStatuses */ + public function test_terminal_status_cannot_transition_to_any_state(BookingStatus $terminal): void + { + $booking = $this->bookingWithStatus($terminal); + + foreach (BookingStatus::cases() as $target) { + $this->assertFalse( + $booking->canTransitionTo($target), + "終態 {$terminal->value} 不應能轉換至 {$target->value}" + ); + } + } + + public static function terminalStatuses(): array + { + return [ + 'completed' => [BookingStatus::Completed], + 'rejected' => [BookingStatus::Rejected], + 'expired' => [BookingStatus::Expired], + 'member_cancelled' => [BookingStatus::MemberCancelled], + 'provider_cancelled' => [BookingStatus::ProviderCancelled], + ]; + } +} diff --git a/tests/Unit/Traits/NormalizesEmailTest.php b/tests/Unit/Traits/NormalizesEmailTest.php new file mode 100644 index 0000000..c24d575 --- /dev/null +++ b/tests/Unit/Traits/NormalizesEmailTest.php @@ -0,0 +1,57 @@ +subject = new class { + use NormalizesEmail; + + public function normalize(string $email): string + { + return $this->normalizeEmail($email); + } + }; + } + + public function test_uppercase_is_lowercased(): void + { + $this->assertSame('user@example.com', $this->subject->normalize('USER@EXAMPLE.COM')); + } + + public function test_leading_and_trailing_whitespace_is_trimmed(): void + { + $this->assertSame('user@example.com', $this->subject->normalize(' user@example.com ')); + } + + public function test_mixed_case_and_whitespace_are_both_normalized(): void + { + $this->assertSame('user@example.com', $this->subject->normalize(' User@Example.COM ')); + } + + public function test_already_normalized_email_is_unchanged(): void + { + $this->assertSame('user@example.com', $this->subject->normalize('user@example.com')); + } + + public function test_tab_whitespace_is_trimmed(): void + { + $this->assertSame('user@example.com', $this->subject->normalize("\tuser@example.com\t")); + } +} -- 2.52.0