@@ -733,8 +733,206 @@ final class ValidUnitExampleTest extends TestCase
733
733
734
734
## Observable behaviour vs implementation details
735
735
736
+ :x : Bad:
737
+
738
+ ``` php
739
+ final class ApplicationService
740
+ {
741
+ public function __construct(private SubscriptionRepositoryInterface $subscriptionRepository) {}
742
+
743
+ public function renewSubscription(int $subscriptionId): bool
744
+ {
745
+ $subscription = $this->subscriptionRepository->findById($subscriptionId);
746
+
747
+ if (!$subscription->getStatus()->isEqual(Status::expired())) {
748
+ return false;
749
+ }
750
+
751
+ $subscription->setStatus(Status::active());
752
+ $subscription->setModifiedAt(new \DateTimeImmutable());
753
+ return true;
754
+ }
755
+ }
756
+ ```
757
+
758
+ ``` php
759
+ final class Subscription
760
+ {
761
+ private Status $status;
762
+
763
+ private \DateTimeImmutable $modifiedAt;
764
+
765
+ public function __construct(Status $status, \DateTimeImmutable $modifiedAt)
766
+ {
767
+ $this->status = $status;
768
+ $this->modifiedAt = $modifiedAt;
769
+ }
770
+
771
+ public function getStatus(): Status
772
+ {
773
+ return $this->status;
774
+ }
775
+
776
+ public function setStatus(Status $status): void
777
+ {
778
+ $this->status = $status;
779
+ }
780
+
781
+ public function getModifiedAt(): \DateTimeImmutable
782
+ {
783
+ return $this->modifiedAt;
784
+ }
785
+
786
+ public function setModifiedAt(\DateTimeImmutable $modifiedAt): void
787
+ {
788
+ $this->modifiedAt = $modifiedAt;
789
+ }
790
+ }
791
+ ```
792
+
793
+ ``` php
794
+ final class InvalidTestExample extends TestCase
795
+ {
796
+ /**
797
+ * @test
798
+ */
799
+ public function renew_an_expired_subscription_is_possible(): void
800
+ {
801
+ $modifiedAt = new \DateTimeImmutable();
802
+ $expiredSubscription = new Subscription(Status::expired(), $modifiedAt);
803
+ $repository = $this->createStub(SubscriptionRepositoryInterface::class);
804
+ $repository->method('findById')->willReturn($expiredSubscription);
805
+ $sut = new ApplicationService($repository);
806
+
807
+ $result = $sut->renewSubscription(1);
808
+
809
+ self::assertEquals(Status::active(), $expiredSubscription->getStatus());
810
+ self::assertGreaterThan($modifiedAt, $expiredSubscription->getModifiedAt());
811
+ self::assertTrue($result);
812
+ }
813
+
814
+ /**
815
+ * @test
816
+ */
817
+ public function renew_an_active_subscription_is_not_possible(): void
818
+ {
819
+ $modifiedAt = new \DateTimeImmutable();
820
+ $activeSubscription = new Subscription(Status::active(), $modifiedAt);
821
+ $repository = $this->createStub(SubscriptionRepositoryInterface::class);
822
+ $repository->method('findById')->willReturn($activeSubscription);
823
+ $sut = new ApplicationService($repository);
824
+
825
+ $result = $sut->renewSubscription(1);
826
+
827
+ self::assertEquals($modifiedAt, $activeSubscription->getModifiedAt());
828
+ self::assertFalse($result);
829
+ }
830
+ }
831
+ ```
832
+
833
+ :heavy_check_mark : Good:
834
+
835
+ ``` php
836
+ final class ApplicationService
837
+ {
838
+ public function __construct(private SubscriptionRepositoryInterface $subscriptionRepository) {}
839
+
840
+ public function renewSubscription(int $subscriptionId): bool
841
+ {
842
+ $subscription = $this->subscriptionRepository->findById($subscriptionId);
843
+ return $subscription->renew(new \DateTimeImmutable());
844
+ }
845
+ }
846
+ ```
847
+
848
+ ``` php
849
+ final class Subscription
850
+ {
851
+ private Status $status;
852
+
853
+ private \DateTimeImmutable $modifiedAt;
854
+
855
+ public function __construct(\DateTimeImmutable $modifiedAt)
856
+ {
857
+ $this->status = Status::new();
858
+ $this->modifiedAt = $modifiedAt;
859
+ }
860
+
861
+ public function renew(\DateTimeImmutable $modifiedAt): bool
862
+ {
863
+ if (!$this->status->isEqual(Status::expired())) {
864
+ return false;
865
+ }
866
+
867
+ $this->status = Status::active();
868
+ $this->modifiedAt = $modifiedAt;
869
+ return true;
870
+ }
871
+
872
+ public function active(\DateTimeImmutable $modifiedAt): void
873
+ {
874
+ //simplified
875
+ $this->status = Status::active();
876
+ $this->modifiedAt = $modifiedAt;
877
+ }
878
+
879
+ public function expire(\DateTimeImmutable $modifiedAt): void
880
+ {
881
+ //simplified
882
+ $this->status = Status::expired();
883
+ $this->modifiedAt = $modifiedAt;
884
+ }
885
+
886
+ public function isActive(): bool
887
+ {
888
+ return $this->status->isEqual(Status::active());
889
+ }
890
+ }
891
+ ```
892
+
893
+ ``` php
894
+ final class ValidTestExample extends TestCase
895
+ {
896
+ /**
897
+ * @test
898
+ */
899
+ public function renew_an_expired_subscription_is_possible(): void
900
+ {
901
+ $expiredSubscription = SubscriptionMother::expired();
902
+ $repository = $this->createStub(SubscriptionRepositoryInterface::class);
903
+ $repository->method('findById')->willReturn($expiredSubscription);
904
+ $sut = new ApplicationService($repository);
905
+
906
+ $result = $sut->renewSubscription(1);
907
+
908
+ // skip checking modifiedAt as it's not a part of observable behaviour. To check this value we
909
+ // would have to add getter for modifiedAt, probably only for tests purposes.
910
+ self::assertTrue($expiredSubscription->isActive());
911
+ self::assertTrue($result);
912
+ }
913
+
914
+ /**
915
+ * @test
916
+ */
917
+ public function renew_an_active_subscription_is_not_possible(): void
918
+ {
919
+ $activeSubscription = SubscriptionMother::active();
920
+ $repository = $this->createStub(SubscriptionRepositoryInterface::class);
921
+ $repository->method('findById')->willReturn($activeSubscription);
922
+ $sut = new ApplicationService($repository);
923
+
924
+ $result = $sut->renewSubscription(1);
925
+
926
+ self::assertTrue($activeSubscription->isActive());
927
+ self::assertFalse($result);
928
+ }
929
+ }
930
+ ```
931
+
736
932
## Unit of behaviour
737
933
934
+
935
+
738
936
## Humble pattern
739
937
740
938
## Trivial test
0 commit comments