From 45ee9b36d7584a44404eb44d1082d2f2c2d89bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Buczyn=CC=81ski?= Date: Mon, 25 Sep 2023 17:37:33 +0200 Subject: [PATCH] Week 8 --- 08-advanced/08-01-exceptions/README.adoc | 15 ++ .../08-01-exceptions/smarttesting/__init__.py | 0 .../smarttesting/customer/__init__.py | 0 .../smarttesting/customer/person.py | 64 +++++++++ .../smarttesting/verifier/__init__.py | 0 .../verifier/customer/__init__.py | 0 .../verification/_01_name_verification.py | 13 ++ .../verification/_03_domain_exception.py | 2 + .../_04_verification_exception.py | 7 + ...name_with_custom_exception_verification.py | 16 +++ .../customer/verification/__init__.py | 0 .../smarttesting/verifier/verification.py | 9 ++ .../08-01-exceptions/tests/__init__.py | 0 .../tests/verifier/__init__.py | 0 .../tests/verifier/customer/__init__.py | 0 .../_02_name_verification_tests.py | 105 ++++++++++++++ .../customer/verification/__init__.py | 0 08-advanced/08-02-multithreading/README.adoc | 23 ++++ .../smarttesting/__init__.py | 0 .../smarttesting/customer/__init__.py | 0 .../smarttesting/customer/customer.py | 28 ++++ .../smarttesting/customer/person.py | 64 +++++++++ .../smarttesting/verifier/__init__.py | 0 .../verifier/application/__init__.py | 0 .../verifier/application/event.py | 7 + .../verifier/application/event_bus.py | 23 ++++ .../application/verification_event.py | 11 ++ .../customer/_01_customer_verifier.py | 83 ++++++++++++ .../customer/_03_verification_listener.py | 31 +++++ .../customer/_04_fraud_alert_handler.py | 37 +++++ .../verifier/customer/__init__.py | 0 .../customer/customer_verification.py | 17 +++ .../customer/customer_verification_result.py | 36 +++++ .../verifier/customer/fraud_alert_task.py | 17 +++ .../customer/verification/__init__.py | 0 .../verifier/customer/verification/age.py | 38 ++++++ .../verification/identification_number.py | 69 ++++++++++ .../verifier/customer/verification/name.py | 36 +++++ .../verifier/customer/verification_result.py | 7 + .../smarttesting/verifier/verification.py | 15 ++ .../08-02-multithreading/tests/__init__.py | 0 .../tests/verifier/__init__.py | 0 .../customer/_02_customer_verifier_tests.py | 124 +++++++++++++++++ .../_05_async_customer_verifier_tests.py | 39 ++++++ ..._async_customer_with_spy_verifier_tests.py | 64 +++++++++ ...7_exception_throwing_verification_tests.py | 74 ++++++++++ .../tests/verifier/customer/__init__.py | 0 .../tests/verifier/customer/conftest.py | 62 +++++++++ 08-advanced/08-03-docs/README.adoc | 16 +++ .../08-03-docs/smarttesting/__init__.py | 0 .../smarttesting/client/__init__.py | 0 .../smarttesting/client/age_verification.py | 11 ++ .../08-03-docs/smarttesting/client/app.py | 31 +++++ .../client/customer_verification_result.py | 36 +++++ .../smarttesting/client/customer_verifier.py | 40 ++++++ .../smarttesting/client/fraud_view.py | 45 ++++++ .../08-03-docs/smarttesting/client/person.py | 46 +++++++ .../smarttesting/client/verification.py | 6 + 08-advanced/08-03-docs/tests/__init__.py | 0 08-advanced/08-04-mutation/README.adoc | 77 +++++++++++ .../08-04-mutation/smarttesting/__init__.py | 0 .../smarttesting/customer/__init__.py | 0 .../smarttesting/customer/customer.py | 28 ++++ .../smarttesting/customer/person.py | 64 +++++++++ .../smarttesting/verifier/__init__.py | 0 .../verifier/application/__init__.py | 0 .../verifier/application/event.py | 7 + .../verifier/application/event_bus.py | 23 ++++ .../application/verification_event.py | 11 ++ .../verifier/customer/__init__.py | 0 .../customer/customer_verification.py | 17 +++ .../customer/customer_verification_result.py | 36 +++++ .../verifier/customer/customer_verifier.py | 22 +++ .../verifier/customer/verification/_01_age.py | 24 ++++ .../customer/verification/__init__.py | 0 .../verification/identification_number.py | 26 ++++ .../verifier/customer/verification/name.py | 15 ++ .../customer/verification_listener.py | 31 +++++ .../verifier/customer/verification_result.py | 7 + .../smarttesting/verifier/verification.py | 15 ++ 08-advanced/08-04-mutation/tests/__init__.py | 0 .../08-04-mutation/tests/verifier/__init__.py | 0 .../tests/verifier/customer/__init__.py | 0 .../customer/customer_verifier_tests.py | 49 +++++++ .../_02_age_verification_tests.py | 128 ++++++++++++++++++ .../customer/verification/__init__.py | 0 ...dentification_number_verification_tests.py | 62 +++++++++ .../verification/name_verification_tests.py | 20 +++ .../customer/verification_listener_tests.py | 12 ++ 89 files changed, 2041 insertions(+) create mode 100644 08-advanced/08-01-exceptions/README.adoc create mode 100644 08-advanced/08-01-exceptions/smarttesting/__init__.py create mode 100644 08-advanced/08-01-exceptions/smarttesting/customer/__init__.py create mode 100644 08-advanced/08-01-exceptions/smarttesting/customer/person.py create mode 100644 08-advanced/08-01-exceptions/smarttesting/verifier/__init__.py create mode 100644 08-advanced/08-01-exceptions/smarttesting/verifier/customer/__init__.py create mode 100644 08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_01_name_verification.py create mode 100644 08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_03_domain_exception.py create mode 100644 08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_04_verification_exception.py create mode 100644 08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_05_name_with_custom_exception_verification.py create mode 100644 08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/__init__.py create mode 100644 08-advanced/08-01-exceptions/smarttesting/verifier/verification.py create mode 100644 08-advanced/08-01-exceptions/tests/__init__.py create mode 100644 08-advanced/08-01-exceptions/tests/verifier/__init__.py create mode 100644 08-advanced/08-01-exceptions/tests/verifier/customer/__init__.py create mode 100644 08-advanced/08-01-exceptions/tests/verifier/customer/verification/_02_name_verification_tests.py create mode 100644 08-advanced/08-01-exceptions/tests/verifier/customer/verification/__init__.py create mode 100644 08-advanced/08-02-multithreading/README.adoc create mode 100644 08-advanced/08-02-multithreading/smarttesting/__init__.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/customer/__init__.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/customer/customer.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/customer/person.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/__init__.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/application/__init__.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/application/event.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/application/event_bus.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/application/verification_event.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/_01_customer_verifier.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/_03_verification_listener.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/_04_fraud_alert_handler.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/__init__.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/customer_verification.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/customer_verification_result.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/fraud_alert_task.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/__init__.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/age.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/identification_number.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/name.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification_result.py create mode 100644 08-advanced/08-02-multithreading/smarttesting/verifier/verification.py create mode 100644 08-advanced/08-02-multithreading/tests/__init__.py create mode 100644 08-advanced/08-02-multithreading/tests/verifier/__init__.py create mode 100644 08-advanced/08-02-multithreading/tests/verifier/customer/_02_customer_verifier_tests.py create mode 100644 08-advanced/08-02-multithreading/tests/verifier/customer/_05_async_customer_verifier_tests.py create mode 100644 08-advanced/08-02-multithreading/tests/verifier/customer/_06_async_customer_with_spy_verifier_tests.py create mode 100644 08-advanced/08-02-multithreading/tests/verifier/customer/_07_exception_throwing_verification_tests.py create mode 100644 08-advanced/08-02-multithreading/tests/verifier/customer/__init__.py create mode 100644 08-advanced/08-02-multithreading/tests/verifier/customer/conftest.py create mode 100644 08-advanced/08-03-docs/README.adoc create mode 100644 08-advanced/08-03-docs/smarttesting/__init__.py create mode 100644 08-advanced/08-03-docs/smarttesting/client/__init__.py create mode 100644 08-advanced/08-03-docs/smarttesting/client/age_verification.py create mode 100644 08-advanced/08-03-docs/smarttesting/client/app.py create mode 100644 08-advanced/08-03-docs/smarttesting/client/customer_verification_result.py create mode 100644 08-advanced/08-03-docs/smarttesting/client/customer_verifier.py create mode 100644 08-advanced/08-03-docs/smarttesting/client/fraud_view.py create mode 100644 08-advanced/08-03-docs/smarttesting/client/person.py create mode 100644 08-advanced/08-03-docs/smarttesting/client/verification.py create mode 100644 08-advanced/08-03-docs/tests/__init__.py create mode 100644 08-advanced/08-04-mutation/README.adoc create mode 100644 08-advanced/08-04-mutation/smarttesting/__init__.py create mode 100644 08-advanced/08-04-mutation/smarttesting/customer/__init__.py create mode 100644 08-advanced/08-04-mutation/smarttesting/customer/customer.py create mode 100644 08-advanced/08-04-mutation/smarttesting/customer/person.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/__init__.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/application/__init__.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/application/event.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/application/event_bus.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/application/verification_event.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/customer/__init__.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verification.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verification_result.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verifier.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/_01_age.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/__init__.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/identification_number.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/name.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/customer/verification_listener.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/customer/verification_result.py create mode 100644 08-advanced/08-04-mutation/smarttesting/verifier/verification.py create mode 100644 08-advanced/08-04-mutation/tests/__init__.py create mode 100644 08-advanced/08-04-mutation/tests/verifier/__init__.py create mode 100644 08-advanced/08-04-mutation/tests/verifier/customer/__init__.py create mode 100644 08-advanced/08-04-mutation/tests/verifier/customer/customer_verifier_tests.py create mode 100644 08-advanced/08-04-mutation/tests/verifier/customer/verification/_02_age_verification_tests.py create mode 100644 08-advanced/08-04-mutation/tests/verifier/customer/verification/__init__.py create mode 100644 08-advanced/08-04-mutation/tests/verifier/customer/verification/identification_number_verification_tests.py create mode 100644 08-advanced/08-04-mutation/tests/verifier/customer/verification/name_verification_tests.py create mode 100644 08-advanced/08-04-mutation/tests/verifier/customer/verification_listener_tests.py diff --git a/08-advanced/08-01-exceptions/README.adoc b/08-advanced/08-01-exceptions/README.adoc new file mode 100644 index 0000000..01dcf46 --- /dev/null +++ b/08-advanced/08-01-exceptions/README.adoc @@ -0,0 +1,15 @@ += Testowanie wyjątków [08-01] + +== Kod + +Przykład weryfikacji po imieniu `_01_name_verification.py`, który loguje na konsoli informacje o kliencie. + +W `_02_name_verification_tests.py` znajdują się testy weryfikujące na różne sposoby rzucany wyjątek. Zaczynamy od najbardziej generycznego testu, który łapie `AttributeError` - `test_should_throw_an_exception_when_checking_verification`. Test przechodzi przez przypadek. `AttributeError` leci, gdyż w fiksturze `_person_without_name` ktoś źle ustawił pole `gender`. `AttributeError` leci z wywołania `person.gender.name`, gdyż string podany zamiast Gender nie ma atrybutu name. + +Możemy weryfikować wiadomość przy rzconym wyjątku tak jak w przypadku testu `test_should_throw_an_exception_when_checking_verification_only`. + +Zakładając, że z jakiegoś powodu domenowego nasza klasa weryfikacyjna nie może obsłużyć błędnych sytuacji i musi rzucić wyjątek, to ten wyjątek powinien być wyjątkiem związanym z cyklem życia naszej aplikacji. Przypuśćmy, że tworzymy sobie wyjątek `_04_verification_exception.VerificationException`, który jako wyjątek domenowy (`_03_domain_exception.DomainException`) może zostać obsłużony gdzieś w innej części naszej aplikacji. + +Nasza klasa weryfikująca mogłaby wówczas wyglądać tak jak `_05_name_with_custom_exception_verification.NameWithCustomExceptionVerification`. + +Test wtedy mógłby dokonywać asercji na podstawie rzuconego wyjątku tak jak w `test_should_fail_verification_when_name_is_invalid` oraz `test_should_fail_verification_if_name_is_invalid_with_explicit_assertion`, jeśli w naszej bibliotece do testowania nie ma możliwości napisania odpowiedniej asercji. diff --git a/08-advanced/08-01-exceptions/smarttesting/__init__.py b/08-advanced/08-01-exceptions/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-01-exceptions/smarttesting/customer/__init__.py b/08-advanced/08-01-exceptions/smarttesting/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-01-exceptions/smarttesting/customer/person.py b/08-advanced/08-01-exceptions/smarttesting/customer/person.py new file mode 100644 index 0000000..7825d55 --- /dev/null +++ b/08-advanced/08-01-exceptions/smarttesting/customer/person.py @@ -0,0 +1,64 @@ +import enum +from dataclasses import dataclass +from datetime import date + + +class Gender(enum.Enum): + MALE = enum.auto() + FEMALE = enum.auto() + + +class Status(enum.Enum): + STUDENT = enum.auto() + NOT_STUDENT = enum.auto() + + +@dataclass +class Person: + """Reprezentuje osobę do zweryfikowania.""" + + _name: str + _surname: str + _date_of_birth: date + _gender: Gender + _national_id_number: str + _status: Status = Status.NOT_STUDENT + + @property + def name(self) -> str: + return self._name + + @property + def surname(self) -> str: + return self._surname + + @property + def date_of_birth(self) -> date: + return self._date_of_birth + + @property + def gender(self) -> Gender: + return self._gender + + @property + def national_id_number(self) -> str: + return self._national_id_number + + @property + def is_student(self) -> bool: + return self._status == Status.STUDENT + + def student(self) -> None: + self._status = Status.STUDENT + + @property + def age(self): + today = date.today() + years_diff = today.year - self._date_of_birth.year + had_birthday_this_year = ( + today.replace(year=self._date_of_birth.year) < self._date_of_birth + ) + if had_birthday_this_year: + years_diff -= 1 + + return years_diff diff --git a/08-advanced/08-01-exceptions/smarttesting/verifier/__init__.py b/08-advanced/08-01-exceptions/smarttesting/verifier/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-01-exceptions/smarttesting/verifier/customer/__init__.py b/08-advanced/08-01-exceptions/smarttesting/verifier/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_01_name_verification.py b/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_01_name_verification.py new file mode 100644 index 0000000..c0e9a8c --- /dev/null +++ b/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_01_name_verification.py @@ -0,0 +1,13 @@ +from smarttesting.customer.person import Person +from smarttesting.verifier.verification import Verification + + +class NameVerification(Verification): + """Weryfikacja po imieniu.""" + + def passes(self, person: Person) -> bool: + print(f"Person's gender is [{person.gender.name}]") + if person.name is None: + raise AttributeError("Name cannot be None") + + return person.name != "" diff --git a/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_03_domain_exception.py b/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_03_domain_exception.py new file mode 100644 index 0000000..4bb51f5 --- /dev/null +++ b/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_03_domain_exception.py @@ -0,0 +1,2 @@ +class DomainException(RuntimeError): + """Wyjątek domenowy.""" diff --git a/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_04_verification_exception.py b/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_04_verification_exception.py new file mode 100644 index 0000000..cf2a900 --- /dev/null +++ b/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_04_verification_exception.py @@ -0,0 +1,7 @@ +from smarttesting.verifier.customer.verification._03_domain_exception import ( + DomainException, +) + + +class VerificationException(DomainException): + """Wyjątek domenowy związany z nieprawidłową weryfikacją.""" diff --git a/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_05_name_with_custom_exception_verification.py b/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_05_name_with_custom_exception_verification.py new file mode 100644 index 0000000..54cda8f --- /dev/null +++ b/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/_05_name_with_custom_exception_verification.py @@ -0,0 +1,16 @@ +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.verification._04_verification_exception import ( + VerificationException, +) +from smarttesting.verifier.verification import Verification + + +class NameWithCustomExceptionVerification(Verification): + """Weryfikacja po nazwisku rzucająca wyjątek w przypadku błędu.""" + + def passes(self, person: Person) -> bool: + print(f"Person's gender is [{person.gender.name}]") + if person.name is None: + raise VerificationException("Name cannot be None.") + + return person.name != "" diff --git a/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/__init__.py b/08-advanced/08-01-exceptions/smarttesting/verifier/customer/verification/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-01-exceptions/smarttesting/verifier/verification.py b/08-advanced/08-01-exceptions/smarttesting/verifier/verification.py new file mode 100644 index 0000000..fb44e92 --- /dev/null +++ b/08-advanced/08-01-exceptions/smarttesting/verifier/verification.py @@ -0,0 +1,9 @@ +import abc + +from smarttesting.customer.person import Person + + +class Verification(abc.ABC): + @abc.abstractmethod + def passes(self, person: Person) -> bool: + pass diff --git a/08-advanced/08-01-exceptions/tests/__init__.py b/08-advanced/08-01-exceptions/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-01-exceptions/tests/verifier/__init__.py b/08-advanced/08-01-exceptions/tests/verifier/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-01-exceptions/tests/verifier/customer/__init__.py b/08-advanced/08-01-exceptions/tests/verifier/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-01-exceptions/tests/verifier/customer/verification/_02_name_verification_tests.py b/08-advanced/08-01-exceptions/tests/verifier/customer/verification/_02_name_verification_tests.py new file mode 100644 index 0000000..d695770 --- /dev/null +++ b/08-advanced/08-01-exceptions/tests/verifier/customer/verification/_02_name_verification_tests.py @@ -0,0 +1,105 @@ +from datetime import date + +import pytest +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.verification import ( + _01_name_verification, + _05_name_with_custom_exception_verification, +) +from smarttesting.verifier.customer.verification._04_verification_exception import ( + VerificationException, +) + + +class Test02NameVerification: + """Wskazówka: Domyślnie pytest łapie wszystko, co printujemy na ekran. + + By wyłączyć to zachowanie, uruchamiamy go z flagą "-s". + """ + + def test_should_throw_an_exception_when_checking_verification(self) -> None: + """Weryfikujemy czy został rzucony bardzo generyczny wyjątek AttributeError. + + Test ten przechodzi nam przypadkowo, gdyż AttributeError leci w innym miejscu + w kodzie niż się spodziewaliśmy. + + Uruchamiając ten test nie widzimy żeby zalogowała nam się linijka z klasy + `_01_name_verification.NameVerification`... + """ + verification = _01_name_verification.NameVerification() + + with pytest.raises(AttributeError): + verification.passes(self._person_without_name()) + + @pytest.mark.xfail + def test_should_throw_an_exception_when_checking_verification_only(self) -> None: + """Poprawiona wersja poprzedniego testu, gdzie tym razem zweryfikujemy + zawartość wiadomości w rzuconym wyjątku. + + Test nie przechodzi (tak się spodziewamy - dekorator @pytest.mark.xfail), gdyż + nie jest rzucany nasz AttributeError pod if'em, tylko automatycznie gdy + próbujemy na stringu "FEMALE" dobrać się do atrybutu `.name`. + + Zakomentuj linijkę `@pytest.mark.xfail`, by zobaczyć komunikat błędu. + + Problem polega na tym, że `_person_without_name` jest źle przygotowana, jednak + nie byliśmy w stanie tego wyłapać poprzednim testem i uzyskaliśmy + false-negative. + """ + verification = _01_name_verification.NameVerification() + + with pytest.raises(AttributeError, match="Name cannot be None"): + verification.passes(self._person_without_name()) + + @pytest.mark.xfail + def test_should_fail_verification_when_name_is_invalid(self) -> None: + """W momencie, w którym nasza aplikacja rzuca wyjątki domenowe, wtedy nasz test + może po prostu spróbować go wyłapać. + + Zakomentuj `@pytest.mark.xfail` żeby zobaczyć, że test się wysypuje gdyż + wyjątek ktory poleci to AttributeError a nie VerificationException. + """ + verification = ( + _05_name_with_custom_exception_verification.NameWithCustomExceptionVerification() + ) + + with pytest.raises(VerificationException): + verification.passes(self._person_without_name()) + + @pytest.mark.xfail + def test_should_fail_verification_if_name_is_invalid_with_explicit_assertion( + self, + ) -> None: + """Koncepcyjnie to samo co powyżej. Do zastosowania w momencie, w którym + używana biblioteka nie posiada helpera do wyjątków, jak pytest czy unittest. + + Łapiemy w try..except wywołanie metody, która powinna rzucić wyjątek. + Koniecznie należy wywalić test, jeżeli wyjątek nie został rzucony!!! + + W sekcji except możemy wykonywać dodatkowe asercje na wyjątku. + """ + verification = ( + _05_name_with_custom_exception_verification.NameWithCustomExceptionVerification() + ) + try: + verification.passes(self._person_without_name()) + pytest.fail("Should fail the verification") + except VerificationException: + pass + + def _person_without_name(self) -> Person: + """Celowo zepsuta instancja Person. + + Ma None zamiast imienia i string zamiast wartości z typu wyliczeniowego Gender. + + Weźmy pod uwagę, że przed takimi błędami zabezpiecza nas mypy + (stąd komentarz # type: ignore by go wyłączyć w tej linii) lub jeżeli dane + pochodzą z zewnątrz (np. z requestu) to wyłapać to powinna nasza biblioteka + do walidacji, np. marshmallow czy pydantic. + + Niemniej, łapanie wbudowanych wyjątków trzeba robić bardzo świadomie i raczej + nie powinniśmy nimi komunikować domenowych sytuacji wyjątkowych. + """ + return Person( + None, "Smith", date.today(), "FEMALE", "00000000000" # type: ignore + ) diff --git a/08-advanced/08-01-exceptions/tests/verifier/customer/verification/__init__.py b/08-advanced/08-01-exceptions/tests/verifier/customer/verification/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-02-multithreading/README.adoc b/08-advanced/08-02-multithreading/README.adoc new file mode 100644 index 0000000..31d4678 --- /dev/null +++ b/08-advanced/08-02-multithreading/README.adoc @@ -0,0 +1,23 @@ += Testowanie kodu wielowątkowego [08-02] + +== Kod + +=== Testowanie wielowątkowe + +W module `smarttesting` - `_01_customer_verifier.py`. Główna klasa biznesowa, do której będziemy mieli sporo przypadków testowych. + +Zaczynamy od testów w `_02_customer_verifier_tests.py`. Pierwsze testy `test_should_return_results_in_order_of_execution` oraz `test_should_work_in_parallel_with_less_constraint` pokazują jak zapięcie się na konkretne wyniki w konkretnej kolejności, tam gdzie nie ma to sensu, może zepsuć nam testy. + +Następnie pokażemy jak weryfikować procesowanie równoległe. Wywołanie metody `_01_customer_verifier.py.CustomerVerifier#verify_async` spowoduje uzyskanie i zapisanie zdarzenia przez komponent `_03_verification_listener.VerificationListener`. Każda z weryfikacji, w osobnym wątku, wyśle zdarzenie, które zostanie odebrane przez `VerificationListener` w różnym czasie. + +Odkomentowany test `test_should_work_in_parallel_without_a_sleep` w klasie testowej z pliku `_02_customer_verifier_tests.py` się wywali, ponieważ zakończy się szybciej niż procesowanie. + +Rozwiązaniem skutecznym, aczkolwiek nieskalującym się i po prostu nie najlepszym, jest umieszczenie oczekiwania przez wątek testu przez X czasu. Przykładem tego jest `Test02CustomerVerifier.test_should_work_in_parallel_with_a_sleep`. Zdecydowanie lepszym rozwiązaniem jest odpytywanie komponentu nasłuchującego na zdarzenia co X czasu, maksymalnie przez Y czasu. Przykład `Test02CustomerVerifier.test_should_work_in_parallel_with_polling`. + +W przypadku procesowania wielowątkowego, najlepiej jest zawsze próbować testować nasz kod jakby był w jednym wątku. W klasie `CustomerVerifier` metoda `found_fraud` uruchamiana metodę w komponencie `_04_fraud_alert_handler.FraudAlertHandler`, która w środku uruchamia kod w swojej metodzie w osobnym wątku. Komponent `_04_fraud_alert_handler.FraudAlertHandler` moglibyśmy przetestować osobno, jednostkowo. To, co możemy zrobić z testem klasy `CustomerVerifier` to przetestowanie czy efekt uboczny w postaci wywołania naszego komponentu wykonał się w odpowiednim czasie. + +Przykładami takich testów są zawarte w plikach `_05_async_customer_verifier_tests` oraz `_06_async_customer_with_spy_verifier_tests`. W pierwszym przypadku mamy test, w którym weryfikujemy czy efekt uboczny zostanie wywołany. Nie ma nawet potrzeby uruchamiania tego kodu w osobnym wątku. Jeśli chcemy natomiast przetestować czy potrafimy rzeczywiście uruchomić test w osobnym wątku to w klasie testowej z pliku `_06_async_customer_with_spy_verifier_tests` tworzymy sobie sztuczną implementację `FraudAlertTask` z dodatkową metodą, którą "owiniemy" używając `wraps` z Mocka. W teście musimy zmienić asercję tak, żeby oczekiwać na wykonanie się metody na naszej "owiniętej" metodzie. Taki typ test-double nazywa się czasem szpiegiem (`spy`). Nie zastępujemy metody całkowicie mockiem. + +=== Testowanie wielowątkowe - obsługa błędów + +Testowanie wielowątkowe - obsługa błędów. Kod produkcyjny - `_01_customer_verifier.CustomerVerifier.verify` oraz `_07_exception_throwing_verification.py`. W teście `Test07ExceptionThrowingVerification` pokazujemy jak wyjątek rzucony w osobnym wątku wpływa na nasz główny wątek i jak możemy temu zaradzić. diff --git a/08-advanced/08-02-multithreading/smarttesting/__init__.py b/08-advanced/08-02-multithreading/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-02-multithreading/smarttesting/customer/__init__.py b/08-advanced/08-02-multithreading/smarttesting/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-02-multithreading/smarttesting/customer/customer.py b/08-advanced/08-02-multithreading/smarttesting/customer/customer.py new file mode 100644 index 0000000..e3abc18 --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/customer/customer.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from uuid import UUID + +from smarttesting.customer.person import Person + + +@dataclass +class Customer: + """Klient. Klasa opakowująca osobę do zweryfikowania.""" + + _uuid: UUID + _person: Person + + @property + def uuid(self) -> UUID: + return self._uuid + + @property + def person(self) -> Person: + return self._person + + @property + def is_student(self) -> bool: + return self._person.is_student + + @property + def student(self): + return self._person.student diff --git a/08-advanced/08-02-multithreading/smarttesting/customer/person.py b/08-advanced/08-02-multithreading/smarttesting/customer/person.py new file mode 100644 index 0000000..7825d55 --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/customer/person.py @@ -0,0 +1,64 @@ +import enum +from dataclasses import dataclass +from datetime import date + + +class Gender(enum.Enum): + MALE = enum.auto() + FEMALE = enum.auto() + + +class Status(enum.Enum): + STUDENT = enum.auto() + NOT_STUDENT = enum.auto() + + +@dataclass +class Person: + """Reprezentuje osobę do zweryfikowania.""" + + _name: str + _surname: str + _date_of_birth: date + _gender: Gender + _national_id_number: str + _status: Status = Status.NOT_STUDENT + + @property + def name(self) -> str: + return self._name + + @property + def surname(self) -> str: + return self._surname + + @property + def date_of_birth(self) -> date: + return self._date_of_birth + + @property + def gender(self) -> Gender: + return self._gender + + @property + def national_id_number(self) -> str: + return self._national_id_number + + @property + def is_student(self) -> bool: + return self._status == Status.STUDENT + + def student(self) -> None: + self._status = Status.STUDENT + + @property + def age(self): + today = date.today() + years_diff = today.year - self._date_of_birth.year + had_birthday_this_year = ( + today.replace(year=self._date_of_birth.year) < self._date_of_birth + ) + if had_birthday_this_year: + years_diff -= 1 + + return years_diff diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/__init__.py b/08-advanced/08-02-multithreading/smarttesting/verifier/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/application/__init__.py b/08-advanced/08-02-multithreading/smarttesting/verifier/application/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/application/event.py b/08-advanced/08-02-multithreading/smarttesting/verifier/application/event.py new file mode 100644 index 0000000..0463e90 --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/application/event.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class Event: + source: Any diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/application/event_bus.py b/08-advanced/08-02-multithreading/smarttesting/verifier/application/event_bus.py new file mode 100644 index 0000000..c19427b --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/application/event_bus.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass +from typing import Dict, List, Protocol, Type + +from smarttesting.verifier.application.event import Event + + +class Listener(Protocol): + def receive(self, event: Event) -> None: + ... + + +@dataclass +class EventBus: + """Prosta implementacja event busa in-memory.""" + + _subscriptions: Dict[Type[Event], List[Listener]] + + def publish(self, event: Event) -> None: + for listener in self._subscriptions.get(type(event), []): + listener.receive(event) + + def __hash__(self) -> int: + return id(self) diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/application/verification_event.py b/08-advanced/08-02-multithreading/smarttesting/verifier/application/verification_event.py new file mode 100644 index 0000000..711ae8c --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/application/verification_event.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + +from smarttesting.verifier.application.event import Event + + +@dataclass(frozen=True) +class VerificationEvent(Event): + """Zdarzenie związane z weryfikacją klienta.""" + + source_description: str + verification_successful: bool diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/_01_customer_verifier.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/_01_customer_verifier.py new file mode 100644 index 0000000..350fc6f --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/_01_customer_verifier.py @@ -0,0 +1,83 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass +from typing import List, Optional, Set + +from smarttesting.customer.customer import Customer +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.customer_verification import CustomerVerification +from smarttesting.verifier.customer.customer_verification_result import ( + CustomerVerificationResult, +) +from smarttesting.verifier.customer.fraud_alert_task import FraudAlertTask +from smarttesting.verifier.customer.verification_result import VerificationResult +from smarttesting.verifier.verification import Verification + + +@dataclass +class CustomerVerifier: + """Weryfikacja czy klient jest oszustem czy nie. + + Przechodzi po różnych implementacjach weryfikacji i jeśli, przy którejś okaże się, + że użytkownik jest oszustem, wówczas odpowiedni rezultat zostanie zwrócony. + """ + + _verifications: Set[Verification] + _fraud_alert_task: Optional[FraudAlertTask] + + def __post_init__(self) -> None: + verifications_count = len(self._verifications) + self._executor = ThreadPoolExecutor(max_workers=verifications_count) + + def verify(self, customer: Customer) -> List[VerificationResult]: + """Wykonuje weryfikacje w wielu wątkach.""" + futures = [] + + for verification in self._verifications: + # zacznij wykonywać wywołania równolegle + future = self._executor.submit(verification.passes, customer.person) + futures.append(future) + + # zwróć listę odpowiedzi w kolejności ukończenia + return [future.result() for future in as_completed(futures)] + + def verify_no_exceptions(self, customer: Customer) -> List[VerificationResult]: + """Wykonuje weryfikacje w wątkach. + + Uznaje wystąpienie wyjątku za nieudaną weryfikację. + """ + futures = [] + + def consider_exception_a_failed_verification( + verification: Verification, person: Person + ) -> VerificationResult: + try: + return verification.passes(person) + except: # pylint: disable=bare-except # noqa: E722 + # PS: nie powinniśmy w kodzie produkcyjnym łapać wszystkich wyjątków + return VerificationResult(verification.name, False) + + for verification in self._verifications: + # zacznij wykonywać wywołania równolegle + future = self._executor.submit( + consider_exception_a_failed_verification, verification, customer.person + ) + futures.append(future) + + # zwróć listę odpowiedzi w kolejności ukończenia + return [future.result() for future in as_completed(futures)] + + def verify_async(self, customer: Customer) -> None: + """Dokonuje weryfikacji w osobnych wątkach.""" + for verification in self._verifications: + self._executor.submit(verification.passes, customer.person) + + def found_fraud(self, customer: Customer) -> None: + """Wysyła notyfikację o znalezionym oszuście.""" + assert self._fraud_alert_task is not None + + result = CustomerVerificationResult.create_failed(customer.uuid) + customer_verification = CustomerVerification(customer.person, result) + self._fraud_alert_task.delay(customer_verification=customer_verification) + + def close(self) -> None: + self._executor.shutdown() diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/_03_verification_listener.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/_03_verification_listener.py new file mode 100644 index 0000000..a7f7e1d --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/_03_verification_listener.py @@ -0,0 +1,31 @@ +import logging +from dataclasses import dataclass, field +from queue import Queue +from typing import List + +from smarttesting.verifier.application.verification_event import VerificationEvent + +logger = logging.getLogger(__name__) + + +@dataclass +class VerificationListener: + """Nasłuchiwacz na zdarzenia weryfikacyjne. Zapisuje je w kolejce.""" + + _queue: Queue = field(default_factory=Queue) + _events: List[VerificationEvent] = field(default_factory=list) + + def receive(self, event: VerificationEvent) -> None: + logger.info("Got an event! %s", event) + self._queue.put(event) + + def _drain(self) -> None: + """Pobierz nowe eventy i doczep je do listy w pamięci.""" + for _ in range(self._queue.qsize()): + event = self._queue.get_nowait() + self._events.append(event) + + @property + def received_events(self) -> List[VerificationEvent]: + self._drain() + return self._events[:] diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/_04_fraud_alert_handler.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/_04_fraud_alert_handler.py new file mode 100644 index 0000000..17f2699 --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/_04_fraud_alert_handler.py @@ -0,0 +1,37 @@ +import logging +import time +from queue import Queue +from threading import Thread + +from smarttesting.verifier.customer.customer_verification import CustomerVerification +from smarttesting.verifier.customer.fraud_alert_task import FraudAlertTask, TaskResult + + +class FraudAlertHandler(FraudAlertTask): + """Implementacja zadania uruchamiająca sobie metodę procesującą w tle.""" + + def __init__(self) -> None: + super().__init__() + self._logger = logging.getLogger(__name__) + + def delay(self, *, customer_verification: CustomerVerification) -> TaskResult: + """'Procesowanie' będzie się odbywać w osobnym wątku. + + Używamy kolejki z biblioteki standardowej która gwarantuje nam thread-safety + oraz prosty sposób do zwrócenia wyniku z wątku. + """ + queue: Queue = Queue(maxsize=1) + + thread = Thread( + target=self._process, args=(customer_verification, queue), daemon=True + ) + thread.start() + + return queue + + def _process( + self, customer_verification: CustomerVerification, queue: Queue + ) -> None: + self._logger.info("Running fraud notification in a new thread") + time.sleep(2) + queue.put_nowait(customer_verification.result) diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/__init__.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/customer_verification.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/customer_verification.py new file mode 100644 index 0000000..b0c9b74 --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/customer_verification.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.customer_verification_result import ( + CustomerVerificationResult, +) + + +@dataclass(frozen=True) +class CustomerVerification: + """Klasa wiadomości, którą wysyłamy poprzez brokera. + + Reprezentuje osobę i rezultat weryfikacji. + """ + + person: Person + result: CustomerVerificationResult diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/customer_verification_result.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/customer_verification_result.py new file mode 100644 index 0000000..3f3006f --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/customer_verification_result.py @@ -0,0 +1,36 @@ +import enum +from dataclasses import dataclass +from uuid import UUID + + +class Status(enum.Enum): + VERIFICATION_PASSED = "VERIFICATION_PASSED" + VERIFICATION_FAILED = "VERIFICATION_FAILED" + + +@dataclass(frozen=True) +class CustomerVerificationResult: + """Rezultat weryfikacji klienta.""" + + _user_id: UUID + _status: Status + + @property + def user_id(self) -> UUID: + return self._user_id + + @property + def status(self) -> Status: + return self._status + + @property + def passed(self) -> bool: + return self._status == Status.VERIFICATION_PASSED + + @staticmethod + def create_passed(user_id: UUID) -> "CustomerVerificationResult": + return CustomerVerificationResult(user_id, Status.VERIFICATION_PASSED) + + @staticmethod + def create_failed(user_id: UUID) -> "CustomerVerificationResult": + return CustomerVerificationResult(user_id, Status.VERIFICATION_FAILED) diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/fraud_alert_task.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/fraud_alert_task.py new file mode 100644 index 0000000..010ab5c --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/fraud_alert_task.py @@ -0,0 +1,17 @@ +from typing import Protocol + +from smarttesting.verifier.customer.customer_verification import CustomerVerification + + +class TaskResult(Protocol): + """Prosty protokół opokowujący AsyncResult z Celery.""" + + def get(self) -> None: + ... + + +class FraudAlertTask(Protocol): + """Prosty protokół opakowaujący taska celery z danym argumentem.""" + + def delay(self, *, customer_verification: CustomerVerification) -> TaskResult: + ... diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/__init__.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/age.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/age.py new file mode 100644 index 0000000..58e0f8b --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/age.py @@ -0,0 +1,38 @@ +import logging +import random +import time +from dataclasses import dataclass + +from smarttesting.customer.person import Person +from smarttesting.verifier.application.event_bus import EventBus +from smarttesting.verifier.application.verification_event import VerificationEvent +from smarttesting.verifier.customer.verification_result import VerificationResult +from smarttesting.verifier.verification import Verification + +logger = logging.getLogger(__name__) + + +@dataclass(unsafe_hash=True) +class AgeVerification(Verification): + """Weryfikacja wieku osoby wnioskującej o udzielenie pożyczki. + + Po zakończonym procesowaniu weryfikacji wysyła zdarzenie z rezultatem weryfikacji. + """ + + _publisher: EventBus + + def passes(self, person: Person) -> VerificationResult: + logger.info("Running age verification") + # Symuluje procesowanie w czasie losowym do 2 sekund + time.sleep(random.randint(0, 2000) / 1000) + logger.info("Age verification done") + + if person.age < 0: + raise ValueError("Age cannot be negative!") + result = 18 <= person.age <= 99 + self._publisher.publish(VerificationEvent(self, self.name, result)) + return VerificationResult(self.name, result) + + @property + def name(self) -> str: + return "age" diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/identification_number.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/identification_number.py new file mode 100644 index 0000000..4f4a793 --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/identification_number.py @@ -0,0 +1,69 @@ +import logging +import random +import time +from dataclasses import dataclass + +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.application.event_bus import EventBus +from smarttesting.verifier.application.verification_event import VerificationEvent +from smarttesting.verifier.customer.verification_result import VerificationResult +from smarttesting.verifier.verification import Verification + +logger = logging.getLogger(__name__) + + +@dataclass(unsafe_hash=True) +class IdentificationNumberVerification(Verification): + """Weryfikacja poprawności numeru PESEL. + + Po zakończonym procesowaniu weryfikacji wysyła zdarzenie z rezultatem weryfikacji. + """ + + _publisher: EventBus + + def passes(self, person: Person) -> VerificationResult: + logger.info("Running age verification") + # Symuluje procesowanie w czasie losowym do 2 sekund + time.sleep(random.randint(0, 2000) / 1000) + logger.info("Id verification done") + + result = ( + self._gender_matches_id_number(person) + and self._starts_with_date_of_birth(person) + and self._weight_is_correct(person) + ) + self._publisher.publish(VerificationEvent(self, self.name, result)) + return VerificationResult(self.name, result) + + @property + def name(self) -> str: + return "id" + + def _gender_matches_id_number(self, person: Person) -> bool: + tenth_character = person.national_id_number[9:10] + if int(tenth_character) % 2 == 0: + return person.gender == Gender.FEMALE + else: + return person.gender == Gender.MALE + + def _starts_with_date_of_birth(self, person: Person) -> bool: + dob_formatted = person.date_of_birth.strftime("%y%m%d") + if dob_formatted[0] == "0": + month = person.date_of_birth.month + 20 + dob_formatted = dob_formatted[:2] + str(month) + dob_formatted[4:] + + return dob_formatted == person.national_id_number[:6] + + def _weight_is_correct(self, person: Person) -> bool: + if len(person.national_id_number) != 11: + return False + + weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3] + weight_sum = sum( + int(person.national_id_number[index]) * weights[index] + for index in range(10) + ) + actual_sum = (10 - weight_sum % 10) % 10 + + check_sum = int(person.national_id_number[10]) + return actual_sum == check_sum diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/name.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/name.py new file mode 100644 index 0000000..067e2ad --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification/name.py @@ -0,0 +1,36 @@ +import logging +import random +import time +from dataclasses import dataclass + +from smarttesting.customer.person import Person +from smarttesting.verifier.application.event_bus import EventBus +from smarttesting.verifier.application.verification_event import VerificationEvent +from smarttesting.verifier.customer.verification_result import VerificationResult +from smarttesting.verifier.verification import Verification + +logger = logging.getLogger(__name__) + + +@dataclass(unsafe_hash=True) +class NameVerification(Verification): + """Weryfikacja po imieniu. + + Po zakończonym procesowaniu weryfikacji wysyła zdarzenie z rezultatem weryfikacji. + """ + + _publisher: EventBus + + def passes(self, person: Person) -> VerificationResult: + logger.info("Running name verification") + # Symuluje procesowanie w czasie losowym do 2 sekund + time.sleep(random.randint(0, 2000) / 1000) + logger.info("Name verification done") + + result = person.name is not None and person.name != "" + self._publisher.publish(VerificationEvent(self, self.name, result)) + return VerificationResult(self.name, result) + + @property + def name(self) -> str: + return "name" diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification_result.py b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification_result.py new file mode 100644 index 0000000..a2a9b6c --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/customer/verification_result.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class VerificationResult: + verification_name: str + result: bool diff --git a/08-advanced/08-02-multithreading/smarttesting/verifier/verification.py b/08-advanced/08-02-multithreading/smarttesting/verifier/verification.py new file mode 100644 index 0000000..5365e6b --- /dev/null +++ b/08-advanced/08-02-multithreading/smarttesting/verifier/verification.py @@ -0,0 +1,15 @@ +import abc + +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.verification_result import VerificationResult + + +class Verification(abc.ABC): + @abc.abstractmethod + def passes(self, person: Person) -> VerificationResult: + pass + + @property + @abc.abstractmethod + def name(self) -> str: + pass diff --git a/08-advanced/08-02-multithreading/tests/__init__.py b/08-advanced/08-02-multithreading/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-02-multithreading/tests/verifier/__init__.py b/08-advanced/08-02-multithreading/tests/verifier/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-02-multithreading/tests/verifier/customer/_02_customer_verifier_tests.py b/08-advanced/08-02-multithreading/tests/verifier/customer/_02_customer_verifier_tests.py new file mode 100644 index 0000000..a4917e2 --- /dev/null +++ b/08-advanced/08-02-multithreading/tests/verifier/customer/_02_customer_verifier_tests.py @@ -0,0 +1,124 @@ +import time + +import polling +import pytest +from smarttesting.customer.customer import Customer +from smarttesting.verifier.customer._01_customer_verifier import CustomerVerifier +from smarttesting.verifier.customer._03_verification_listener import ( + VerificationListener, +) + + +class Test02CustomerVerifier: + """Klasa testowa procesowania weryfikacji na wielu wątkach.""" + + @pytest.mark.skip + @pytest.mark.flaky(reruns=5) + def test_should_return_results_in_order_of_execution( + self, verifier: CustomerVerifier, stefan: Customer + ) -> None: + """Test uruchamia procesowanie klienta Stefan. + + Procesowanie zostanie ukończone w losowej kolejności, natomiast w naszym teście + oczekujemy, że dostaniemy odpowiedź w kolejności weryfikacji age, id i na końcu + name. + + Na wszelki wypadek uruchamiamy test 5 razy, żeby upewnić się, że za każdym + razem przejdzie. + + Odkomentuj dekorator `@pytest.mark.skip`, żeby przekonać się, że test może nie + przejść! + """ + results = verifier.verify(stefan) + + verification_results_names = [result.verification_name for result in results] + assert verification_results_names == ["age", "id", "name"] + + def test_should_work_in_parallel_with_less_constraint( + self, verifier: CustomerVerifier, stefan: Customer + ) -> None: + """Test uruchamia procesowanie klienta Stefan. + + Procesowanie zostanie ukończone w losowej kolejności. W naszym teście + oczekujemy, że dostaniemy odpowiedź zawierającą wszystkie 3 weryfikacje w + losowej kolejności. + """ + results = verifier.verify(stefan) + + verification_results_names = [result.verification_name for result in results] + assert set(verification_results_names) == {"age", "id", "name"} + + @pytest.mark.skip + def test_should_work_in_parallel_without_a_sleep( + self, + verifier: CustomerVerifier, + stefan: Customer, + verifications_listener: VerificationListener, + ) -> None: + """Testujemy asynchroniczne procesowanie zdarzeń. + + Po każdej weryfikacji zostaje wysłane zdarzenie, które komponent + `_03_verification_listener.VerificationListener` trzyma w kolejce. + + Procesowanie jest asynchroniczne, a test na to nie reaguje. Po zakolejkowaniu + wywołań asynchronicznych od razu przechodzi do asercji zapisanych zdarzeń w + kolejce. Problem w tym, że procesowanie jeszcze trwa! Innymi słowy test jest + szybszy niż kod, który testuje. + + Odkomentuj dekorator `@pytest.mark.skip`, żeby przekonać się, że test może nie + przejść! + """ + verifier.verify_async(stefan) + + events = verifications_listener.received_events + source_descriptions = [event.source_description for event in events] + assert set(source_descriptions) == {"age", "id", "name"} + + def test_should_work_in_parallel_with_a_sleep( + self, + verifier: CustomerVerifier, + stefan: Customer, + verifications_listener: VerificationListener, + ) -> None: + """Próba naprawy sytuacji z testu powyżej. + + Zakładamy, że w ciągu 4 sekund zadania powinny się ukończyć, a zdarzenia + powinny zostać wysłane. + + Rozwiązanie to w żaden sposób się nie skaluje i jest marnotrawstwem czasu. + W momencie, w którym procesowanie ukończy się po np. 100 ms, zmarnujemy + 3.9 sekundy by dokonać asercji. + """ + verifier.verify_async(stefan) + + time.sleep(4) + + events = verifications_listener.received_events + source_descriptions = [event.source_description for event in events] + assert set(source_descriptions) == {"age", "id", "name"} + + def test_should_work_in_parallel_with_polling( + self, + verifier: CustomerVerifier, + stefan: Customer, + verifications_listener: VerificationListener, + ) -> None: + """Najlepsze rozwiązanie problemu. + + Wykorzystujemy bibliotekę polling która wykorzystuje okresowe sprawdzenia + z maksymalnym timeout'em - czyli tak zwany polling :) Wykorzystamy ją, + dokonując sprawdzenia co 100ms, czekając maksymalnie 5s. W ten sposób nasz + test w pesymistycznym przypadku zmarnuje tylko 100ms. + """ + verifier.verify_async(stefan) + + polling.poll( + # czekamy, aż wszystkie zdarzenia zostaną odebrane + # moglibyśmy też umieścić tu warunek z asercji z końca testu + lambda: len(verifications_listener.received_events) == 3, + step=0.1, + timeout=5, + ) + events = verifications_listener.received_events + source_descriptions = [event.source_description for event in events] + assert set(source_descriptions) == {"age", "id", "name"} diff --git a/08-advanced/08-02-multithreading/tests/verifier/customer/_05_async_customer_verifier_tests.py b/08-advanced/08-02-multithreading/tests/verifier/customer/_05_async_customer_verifier_tests.py new file mode 100644 index 0000000..0c611b0 --- /dev/null +++ b/08-advanced/08-02-multithreading/tests/verifier/customer/_05_async_customer_verifier_tests.py @@ -0,0 +1,39 @@ +# pylint: disable=redefined-outer-name +from typing import Set +from unittest.mock import Mock + +import pytest +from smarttesting.customer.customer import Customer +from smarttesting.verifier.customer._01_customer_verifier import CustomerVerifier +from smarttesting.verifier.customer._04_fraud_alert_handler import FraudAlertHandler +from smarttesting.verifier.customer.fraud_alert_task import FraudAlertTask +from smarttesting.verifier.verification import Verification + + +@pytest.fixture() +def fraud_alert_handler() -> FraudAlertTask: + return Mock(spec_set=FraudAlertHandler) + + +@pytest.fixture() +def verifier( + verifications: Set[Verification], fraud_alert_handler: FraudAlertTask +) -> CustomerVerifier: + return CustomerVerifier( + _verifications=verifications, + _fraud_alert_task=fraud_alert_handler, + ) + + +class Test05AsyncCustomerVerifier: + """Test weryfikujący efekt uboczny w postaci wywołania metody asynchronicznie.""" + + def test_should_notify_about_fraud( + self, + verifier: CustomerVerifier, + stefan: Customer, + fraud_alert_handler: Mock, + ) -> None: + verifier.found_fraud(stefan) + + fraud_alert_handler.delay.assert_called_once() diff --git a/08-advanced/08-02-multithreading/tests/verifier/customer/_06_async_customer_with_spy_verifier_tests.py b/08-advanced/08-02-multithreading/tests/verifier/customer/_06_async_customer_with_spy_verifier_tests.py new file mode 100644 index 0000000..6d2ba4e --- /dev/null +++ b/08-advanced/08-02-multithreading/tests/verifier/customer/_06_async_customer_with_spy_verifier_tests.py @@ -0,0 +1,64 @@ +# pylint: disable=redefined-outer-name +from queue import Queue +from typing import Set +from unittest.mock import patch + +import polling +import pytest +from smarttesting.customer.customer import Customer +from smarttesting.verifier.customer._01_customer_verifier import CustomerVerifier +from smarttesting.verifier.customer._04_fraud_alert_handler import FraudAlertHandler +from smarttesting.verifier.customer.customer_verification import CustomerVerification +from smarttesting.verifier.customer.fraud_alert_task import FraudAlertTask +from smarttesting.verifier.verification import Verification + + +class SpyReadyHandler(FraudAlertHandler): + """Nadpisujemy tylko metodę wykonującą pracę w osobnym wątku.""" + + def _process( + self, customer_verification: CustomerVerification, queue: Queue + ) -> None: + self.spy_on_me() + queue.put_nowait(customer_verification.result) + + def spy_on_me(self) -> None: + self._logger.info("Hello") + + +@pytest.fixture() +def fraud_alert_handler() -> FraudAlertTask: + return SpyReadyHandler() + + +@pytest.fixture() +def verifier( + verifications: Set[Verification], fraud_alert_handler: FraudAlertTask +) -> CustomerVerifier: + return CustomerVerifier( + _verifications=verifications, + _fraud_alert_task=fraud_alert_handler, + ) + + +class Test06AsyncCustomerWithSpyVerifier: + """Test weryfikujący efekt uboczny w postaci wywołania metody asynchronicznie.""" + + def test_should_delegate_work_to_a_separate_thread( + self, + verifier: CustomerVerifier, + stefan: Customer, + fraud_alert_handler: SpyReadyHandler, + ) -> None: + with patch.object( + SpyReadyHandler, "spy_on_me", wraps=fraud_alert_handler.spy_on_me + ) as wrapped_spy_on_me: + verifier.found_fraud(stefan) + + # musi być pod contextmanagerem, inaczej szpieg zostanie wyłączony + polling.poll( + lambda: wrapped_spy_on_me.called, + step=0.1, + timeout=5, + ) + wrapped_spy_on_me.assert_called_once() diff --git a/08-advanced/08-02-multithreading/tests/verifier/customer/_07_exception_throwing_verification_tests.py b/08-advanced/08-02-multithreading/tests/verifier/customer/_07_exception_throwing_verification_tests.py new file mode 100644 index 0000000..a354738 --- /dev/null +++ b/08-advanced/08-02-multithreading/tests/verifier/customer/_07_exception_throwing_verification_tests.py @@ -0,0 +1,74 @@ +import logging + +import polling +import pytest +from smarttesting.customer.customer import Customer +from smarttesting.customer.person import Person +from smarttesting.verifier.customer._01_customer_verifier import CustomerVerifier +from smarttesting.verifier.customer.verification_result import VerificationResult +from smarttesting.verifier.verification import Verification + + +@pytest.fixture() +def verifier() -> CustomerVerifier: + return CustomerVerifier( + _verifications={ExceptionThrowingVerification()}, + _fraud_alert_task=None, + ) + + +class ExceptionThrowingVerification(Verification): + def __init__(self) -> None: + self._logger = logging.getLogger(__name__) + + def passes(self, person: Person) -> VerificationResult: + self._logger.info("Running this in a separate thread") + raise ValueError("Boom!") + + @property + def name(self) -> str: + return "exception" + + +class Test07ExceptionThrowingVerification: + @pytest.mark.skip + def test_should_handle_exceptions_gracefully_when_dealing_with_results( + self, + verifier: CustomerVerifier, # pylint: disable=redefined-outer-name + stefan: Customer, + ) -> None: + """Zakładamy, z punktu widzenia biznesowego, że potrafimy obsłużyć sytuację + rzucenia wyjątku. + + W naszym przypadku jest to uzyskanie wyniku procesowania klienta nawet jeśli + wyjątek został rzucony. Nie chcemy sytuacji, w której rzucony błąd wpłynie na nasz + proces biznesowy. + + Odkomentuj dekorator `@pytest.mark.skip`, żeby przekonać się, że test może + nie przejść! + """ + results = verifier.verify(stefan) + + polling.poll( + lambda: results == [VerificationResult("exception", False)], + step=0.1, + timeout=5, + ) + + def test_should_handle_exceptions_gracefully_when_dealing_with_results_passing( + self, + verifier: CustomerVerifier, # pylint: disable=redefined-outer-name + stefan: Customer, + ) -> None: + """ + Poprawiamy problem z kodu wyżej. Metoda produkcyjna + `CustomerVerifier.verify_no_exceptions` potrafi obsłużyć rzucony wyjątek z + osobnego wątku. + """ + results = verifier.verify_no_exceptions(stefan) + + polling.poll( + lambda: results == [VerificationResult("exception", False)], + step=0.1, + timeout=5, + ) diff --git a/08-advanced/08-02-multithreading/tests/verifier/customer/__init__.py b/08-advanced/08-02-multithreading/tests/verifier/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-02-multithreading/tests/verifier/customer/conftest.py b/08-advanced/08-02-multithreading/tests/verifier/customer/conftest.py new file mode 100644 index 0000000..291bfb7 --- /dev/null +++ b/08-advanced/08-02-multithreading/tests/verifier/customer/conftest.py @@ -0,0 +1,62 @@ +# pylint: disable=redefined-outer-name +import uuid +from datetime import date +from typing import Set, cast + +import pytest +from smarttesting.customer.customer import Customer +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.application.event_bus import EventBus, Listener +from smarttesting.verifier.application.verification_event import VerificationEvent +from smarttesting.verifier.customer._01_customer_verifier import CustomerVerifier +from smarttesting.verifier.customer._03_verification_listener import ( + VerificationListener, +) +from smarttesting.verifier.customer.verification.age import AgeVerification +from smarttesting.verifier.customer.verification.identification_number import ( + IdentificationNumberVerification, +) +from smarttesting.verifier.customer.verification.name import NameVerification +from smarttesting.verifier.verification import Verification + + +@pytest.fixture() +def verifications_listener() -> VerificationListener: + return VerificationListener() + + +@pytest.fixture() +def publisher(verifications_listener: VerificationListener) -> EventBus: + listener = cast(Listener, verifications_listener) + return EventBus({VerificationEvent: [listener]}) + + +@pytest.fixture() +def verifications(publisher: EventBus) -> Set[Verification]: + return { + AgeVerification(publisher), + IdentificationNumberVerification(publisher), + NameVerification(publisher), + } + + +@pytest.fixture() +def verifier(verifications: Set[Verification]) -> CustomerVerifier: + return CustomerVerifier( + _verifications=verifications, + _fraud_alert_task=None, + ) + + +@pytest.fixture() +def stefan() -> Customer: + return Customer( + _uuid=uuid.uuid4(), + _person=Person( + _name="", + _surname="", + _date_of_birth=date.today(), + _gender=Gender.MALE, + _national_id_number="0123456789", + ), + ) diff --git a/08-advanced/08-03-docs/README.adoc b/08-advanced/08-03-docs/README.adoc new file mode 100644 index 0000000..8a103df --- /dev/null +++ b/08-advanced/08-03-docs/README.adoc @@ -0,0 +1,16 @@ += Automatyczna dokumentacja API i możliwość jego testowania [08-03] + +== Kod + +Użyty w przykładzie framework FastAPI posiada integrację Out-Of-The-box z OpenAPI (wcześniej znany jako Swagger), o ile używamy Pydantica do walidacji (polecane, pokazywane w tutorialu połączenie). + +Do przejrzenia `smarttesting.client.person.Person`, dziedziczące w tym przykładzie po `pydantic.BaseModel` oraz widok `smarttesting.client.fraud_view.fraud_check_view`. + +== Uruchamianie aplikacji + +``` +cd 08-advanced/08-03-docs/ +uvicorn smarttesting.client.app:app +``` + +Następnie otwieramy w przeglądarce adres `http://127.0.0.1:8000/docs` i cieszymy się automatycznie skonfigurowanym swaggerem :) diff --git a/08-advanced/08-03-docs/smarttesting/__init__.py b/08-advanced/08-03-docs/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-03-docs/smarttesting/client/__init__.py b/08-advanced/08-03-docs/smarttesting/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-03-docs/smarttesting/client/age_verification.py b/08-advanced/08-03-docs/smarttesting/client/age_verification.py new file mode 100644 index 0000000..ca958b9 --- /dev/null +++ b/08-advanced/08-03-docs/smarttesting/client/age_verification.py @@ -0,0 +1,11 @@ +from smarttesting.client.person import Person +from smarttesting.client.verification import Verification + + +class AgeVerification(Verification): + """Weryfikacja wieku osoby wnioskującej o udzielenie pożyczki.""" + + def passes(self, person: Person) -> bool: + if person.age < 0: + raise ValueError("Age cannot be negative!") + return 18 <= person.age <= 99 diff --git a/08-advanced/08-03-docs/smarttesting/client/app.py b/08-advanced/08-03-docs/smarttesting/client/app.py new file mode 100644 index 0000000..e4f1190 --- /dev/null +++ b/08-advanced/08-03-docs/smarttesting/client/app.py @@ -0,0 +1,31 @@ +import logging +from typing import List, Optional + +from fastapi import FastAPI +from injector import Injector, Module, provider +from smarttesting.client.age_verification import AgeVerification +from smarttesting.client.customer_verifier import CustomerVerifier +from smarttesting.client.fraud_view import app as vanilla_app + + +class FraudModule(Module): + @provider + def customer_verifier(self) -> CustomerVerifier: + return CustomerVerifier({AgeVerification()}) + + +def create_app(modules: Optional[List[Module]] = None) -> FastAPI: + """Dla uruchamiania lokalnie/produkcyjnie z całym serwerem.""" + + logging.basicConfig( + level=logging.INFO, format="%(levelname)s: %(asctime)s:%(message)s" + ) + logging.getLogger("uvicorn").propagate = False + + if not modules: + modules = [FraudModule()] + vanilla_app.state.injector = Injector(modules) + return vanilla_app + + +app = create_app() diff --git a/08-advanced/08-03-docs/smarttesting/client/customer_verification_result.py b/08-advanced/08-03-docs/smarttesting/client/customer_verification_result.py new file mode 100644 index 0000000..72de620 --- /dev/null +++ b/08-advanced/08-03-docs/smarttesting/client/customer_verification_result.py @@ -0,0 +1,36 @@ +import enum +from dataclasses import dataclass +from uuid import UUID + + +class Status(enum.Enum): + VERIFICATION_PASSED = enum.auto() + VERIFICATION_FAILED = enum.auto() + + +@dataclass(frozen=True) +class CustomerVerificationResult: + """Rezultat weryfikacji klienta.""" + + _user_id: UUID + _status: Status + + @property + def user_id(self) -> UUID: + return self._user_id + + @property + def status(self) -> Status: + return self._status + + @property + def passed(self) -> bool: + return self._status == Status.VERIFICATION_PASSED + + @staticmethod + def create_passed(user_id: UUID) -> "CustomerVerificationResult": + return CustomerVerificationResult(user_id, Status.VERIFICATION_PASSED) + + @staticmethod + def create_failed(user_id: UUID) -> "CustomerVerificationResult": + return CustomerVerificationResult(user_id, Status.VERIFICATION_FAILED) diff --git a/08-advanced/08-03-docs/smarttesting/client/customer_verifier.py b/08-advanced/08-03-docs/smarttesting/client/customer_verifier.py new file mode 100644 index 0000000..62859a2 --- /dev/null +++ b/08-advanced/08-03-docs/smarttesting/client/customer_verifier.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass +from typing import Set, Union + +from smarttesting.client.customer_verification_result import CustomerVerificationResult +from smarttesting.client.person import Person +from smarttesting.client.verification import Verification + + +@dataclass(init=False) +class CustomerVerifier: + """Weryfikacja czy klient jest oszustem czy nie. + + Przechodzi po różnych implementacjach weryfikacji i zwraca zagregowany wynik. + """ + + _verifications: Set[Verification] + + def __init__(self, *args: Union[Verification, Set[Verification]]): + """ + Initializer wspierający dwie metody tworzenia obiektu. + """ + self._verifications = set() + for arg in args: + if isinstance(arg, set): + self._verifications.update(arg) + else: + self._verifications.add(arg) + + def verify(self, person: Person) -> CustomerVerificationResult: + """ + Główna metoda biznesowa. Weryfikuje czy dana osoba jest oszustem. + """ + verifications_passed = all( + verification.passes(person) for verification in self._verifications + ) + + if verifications_passed: + return CustomerVerificationResult.create_passed(person.uuid) + else: + return CustomerVerificationResult.create_failed(person.uuid) diff --git a/08-advanced/08-03-docs/smarttesting/client/fraud_view.py b/08-advanced/08-03-docs/smarttesting/client/fraud_view.py new file mode 100644 index 0000000..f43c842 --- /dev/null +++ b/08-advanced/08-03-docs/smarttesting/client/fraud_view.py @@ -0,0 +1,45 @@ +import logging +from typing import Any, Type, TypeVar + +from fastapi import Depends, FastAPI, Request, Response, status +from smarttesting.client.customer_verification_result import Status +from smarttesting.client.customer_verifier import CustomerVerifier +from smarttesting.client.person import Person + +logger = logging.getLogger(__name__) + + +app = FastAPI() + + +TypeToInject = TypeVar("TypeToInject") + + +def Injects(item: Type[TypeToInject]) -> TypeToInject: # pylint: disable=invalid-name + """Minimalna integracja FastAPI z Injectorem. + + Zapewnia nam wstrzykiwanie pożądanych obiektów do widoku o ile "otoczymy" je + wywołaniem Injects(DesiredClass), analogicznie jak wbudowany w FastAPI Depends. + """ + + def inject(request: Request) -> Any: + return request.app.state.injector.get(item) + + return Depends(inject) + + +@app.post("/fraudCheck", status_code=status.HTTP_200_OK) +def fraud_check_view( + person: Person, + response: Response, + customer_verifier=Injects(CustomerVerifier), +): + """ + Widok dla żądania sprawdzenia potencjalnego fraudu. + + Zwraca status 200 dla osoby uczciwej, 401 dla oszusta. + """ + logger.info("Received a verification request for person %s", person) + result = customer_verifier.verify(person) + if result.status == Status.VERIFICATION_FAILED: + response.status_code = status.HTTP_401_UNAUTHORIZED diff --git a/08-advanced/08-03-docs/smarttesting/client/person.py b/08-advanced/08-03-docs/smarttesting/client/person.py new file mode 100644 index 0000000..eab8387 --- /dev/null +++ b/08-advanced/08-03-docs/smarttesting/client/person.py @@ -0,0 +1,46 @@ +import enum +from datetime import date +from uuid import UUID + +from pydantic import BaseModel + + +class Gender(enum.Enum): + MALE = "MALE" + FEMALE = "FEMALE" + + +class Status(enum.Enum): + STUDENT = "STUDENT" + NOT_STUDENT = "NOT_STUDENT" + + +class Person(BaseModel): + """Reprezentuje osobę do zweryfikowania.""" + + uuid: UUID + name: str + surname: str + date_of_birth: date + gender: Gender + national_id_number: str + status: Status = Status.NOT_STUDENT + + @property + def is_student(self) -> bool: + return self.status == Status.STUDENT + + def student(self) -> None: + self.status = Status.STUDENT + + @property + def age(self): + today = date.today() + years_diff = today.year - self.date_of_birth.year + had_birthday_this_year = ( + today.replace(year=self.date_of_birth.year) < self.date_of_birth + ) + if had_birthday_this_year: + years_diff -= 1 + + return years_diff diff --git a/08-advanced/08-03-docs/smarttesting/client/verification.py b/08-advanced/08-03-docs/smarttesting/client/verification.py new file mode 100644 index 0000000..15a29fc --- /dev/null +++ b/08-advanced/08-03-docs/smarttesting/client/verification.py @@ -0,0 +1,6 @@ +from smarttesting.client.person import Person + + +class Verification: + def passes(self, _person: Person) -> bool: + return False diff --git a/08-advanced/08-03-docs/tests/__init__.py b/08-advanced/08-03-docs/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/README.adoc b/08-advanced/08-04-mutation/README.adoc new file mode 100644 index 0000000..fb94530 --- /dev/null +++ b/08-advanced/08-04-mutation/README.adoc @@ -0,0 +1,77 @@ += Testy mutacyjne [08-04] + +== Kod + +Najpierw `_01_age.AgeVerification` jako implementacja, którą będziemy testować. + +Potem klasa testowa `_02_age_verification_tests.Test02AgeVerification`, a w niej: + +- `test_should_throw_exception_when_age_invalid` +- `test_should_return_positive_verification_when_age_is_within_the_threshold` +- `test_should_return_negative_verification_when_age_is_below_the_threshold` +- `test_should_return_negative_verification_when_age_is_above_the_threshold` + +Czyli weryfikujemy + +* wiek z przyszłości +* wiek w ramach przedziału akceptowalnego +* wiek poniżej przedziału +* wiek powyżej przedziału + +Jak uruchomimy narzędzie do policzenia pokrycia kodu testami + +``` +cd 08-advanced/08-04-mutation +pytest --cov=smarttesting tests/ +``` + +to wyjdzie nam 100% pokrycia kodu w klasach weryfikujących. Pytanie jest czy wszystkie ścieżki zostały rzeczywiście pokryte? Zapomnieliśmy o warunkach brzegowych! + +Jeśli uruchomimy: + +``` +cd 08-advanced/08-04-mutation/ +mutmut run --paths-to-mutate smarttesting/verifier/customer/verification/_01_age.py +``` + +Zobaczymy, że z wygenerowanych 14 mutacji aż 9 nie zostało wykrytych naszymi testami! + +``` +- Mutation testing starting - + +... + +Legend for output: +🎉 Killed mutants. The goal is for everything to end up in this bucket. +⏰ Timeout. Test suite took 10 times as long as the baseline so were killed. +🤔 Suspicious. Tests took a long time, but not long enough to be fatal. +🙁 Survived. This means your tests needs to be expanded. +🔇 Skipped. Skipped. + +mutmut cache is out of date, clearing it... +1. Running tests without mutations +⠦ Running...Done + +2. Checking mutants +⠴ 14/14 🎉 5 ⏰ 0 🤔 0 🙁 9 🔇 0 +``` + +Dodatkowe informacje możemy uzyskać prosząc o raport: +``` +mutmut results +``` + +``` +To apply a mutant on disk: + mutmut apply + +To show a mutant: + mutmut show + + +Survived 🙁 (9) + +---- smarttesting/verifier/customer/verification/_01_age.py (9) ---- + +5-11, 13-14 +``` diff --git a/08-advanced/08-04-mutation/smarttesting/__init__.py b/08-advanced/08-04-mutation/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/smarttesting/customer/__init__.py b/08-advanced/08-04-mutation/smarttesting/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/smarttesting/customer/customer.py b/08-advanced/08-04-mutation/smarttesting/customer/customer.py new file mode 100644 index 0000000..e3abc18 --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/customer/customer.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from uuid import UUID + +from smarttesting.customer.person import Person + + +@dataclass +class Customer: + """Klient. Klasa opakowująca osobę do zweryfikowania.""" + + _uuid: UUID + _person: Person + + @property + def uuid(self) -> UUID: + return self._uuid + + @property + def person(self) -> Person: + return self._person + + @property + def is_student(self) -> bool: + return self._person.is_student + + @property + def student(self): + return self._person.student diff --git a/08-advanced/08-04-mutation/smarttesting/customer/person.py b/08-advanced/08-04-mutation/smarttesting/customer/person.py new file mode 100644 index 0000000..7825d55 --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/customer/person.py @@ -0,0 +1,64 @@ +import enum +from dataclasses import dataclass +from datetime import date + + +class Gender(enum.Enum): + MALE = enum.auto() + FEMALE = enum.auto() + + +class Status(enum.Enum): + STUDENT = enum.auto() + NOT_STUDENT = enum.auto() + + +@dataclass +class Person: + """Reprezentuje osobę do zweryfikowania.""" + + _name: str + _surname: str + _date_of_birth: date + _gender: Gender + _national_id_number: str + _status: Status = Status.NOT_STUDENT + + @property + def name(self) -> str: + return self._name + + @property + def surname(self) -> str: + return self._surname + + @property + def date_of_birth(self) -> date: + return self._date_of_birth + + @property + def gender(self) -> Gender: + return self._gender + + @property + def national_id_number(self) -> str: + return self._national_id_number + + @property + def is_student(self) -> bool: + return self._status == Status.STUDENT + + def student(self) -> None: + self._status = Status.STUDENT + + @property + def age(self): + today = date.today() + years_diff = today.year - self._date_of_birth.year + had_birthday_this_year = ( + today.replace(year=self._date_of_birth.year) < self._date_of_birth + ) + if had_birthday_this_year: + years_diff -= 1 + + return years_diff diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/__init__.py b/08-advanced/08-04-mutation/smarttesting/verifier/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/application/__init__.py b/08-advanced/08-04-mutation/smarttesting/verifier/application/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/application/event.py b/08-advanced/08-04-mutation/smarttesting/verifier/application/event.py new file mode 100644 index 0000000..0463e90 --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/application/event.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class Event: + source: Any diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/application/event_bus.py b/08-advanced/08-04-mutation/smarttesting/verifier/application/event_bus.py new file mode 100644 index 0000000..c19427b --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/application/event_bus.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass +from typing import Dict, List, Protocol, Type + +from smarttesting.verifier.application.event import Event + + +class Listener(Protocol): + def receive(self, event: Event) -> None: + ... + + +@dataclass +class EventBus: + """Prosta implementacja event busa in-memory.""" + + _subscriptions: Dict[Type[Event], List[Listener]] + + def publish(self, event: Event) -> None: + for listener in self._subscriptions.get(type(event), []): + listener.receive(event) + + def __hash__(self) -> int: + return id(self) diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/application/verification_event.py b/08-advanced/08-04-mutation/smarttesting/verifier/application/verification_event.py new file mode 100644 index 0000000..711ae8c --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/application/verification_event.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + +from smarttesting.verifier.application.event import Event + + +@dataclass(frozen=True) +class VerificationEvent(Event): + """Zdarzenie związane z weryfikacją klienta.""" + + source_description: str + verification_successful: bool diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/customer/__init__.py b/08-advanced/08-04-mutation/smarttesting/verifier/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verification.py b/08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verification.py new file mode 100644 index 0000000..b0c9b74 --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verification.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.customer_verification_result import ( + CustomerVerificationResult, +) + + +@dataclass(frozen=True) +class CustomerVerification: + """Klasa wiadomości, którą wysyłamy poprzez brokera. + + Reprezentuje osobę i rezultat weryfikacji. + """ + + person: Person + result: CustomerVerificationResult diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verification_result.py b/08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verification_result.py new file mode 100644 index 0000000..3f3006f --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verification_result.py @@ -0,0 +1,36 @@ +import enum +from dataclasses import dataclass +from uuid import UUID + + +class Status(enum.Enum): + VERIFICATION_PASSED = "VERIFICATION_PASSED" + VERIFICATION_FAILED = "VERIFICATION_FAILED" + + +@dataclass(frozen=True) +class CustomerVerificationResult: + """Rezultat weryfikacji klienta.""" + + _user_id: UUID + _status: Status + + @property + def user_id(self) -> UUID: + return self._user_id + + @property + def status(self) -> Status: + return self._status + + @property + def passed(self) -> bool: + return self._status == Status.VERIFICATION_PASSED + + @staticmethod + def create_passed(user_id: UUID) -> "CustomerVerificationResult": + return CustomerVerificationResult(user_id, Status.VERIFICATION_PASSED) + + @staticmethod + def create_failed(user_id: UUID) -> "CustomerVerificationResult": + return CustomerVerificationResult(user_id, Status.VERIFICATION_FAILED) diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verifier.py b/08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verifier.py new file mode 100644 index 0000000..d610737 --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/customer/customer_verifier.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass +from typing import List, Set + +from smarttesting.customer.customer import Customer +from smarttesting.verifier.customer.verification_result import VerificationResult +from smarttesting.verifier.verification import Verification + + +@dataclass +class CustomerVerifier: + """Weryfikacja czy klient jest oszustem czy nie. + + Przechodzi po różnych implementacjach weryfikacji i jeśli, przy którejś okaże się, + że użytkownik jest oszustem, wówczas odpowiedni rezultat zostanie zwrócony. + """ + + _verifications: Set[Verification] + + def verify(self, customer: Customer) -> List[VerificationResult]: + return [ + verification.passes(customer.person) for verification in self._verifications + ] diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/_01_age.py b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/_01_age.py new file mode 100644 index 0000000..442f1cc --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/_01_age.py @@ -0,0 +1,24 @@ +import logging + +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.verification_result import VerificationResult +from smarttesting.verifier.verification import Verification + +logger = logging.getLogger(__name__) + + +class AgeVerification(Verification): + """Weryfikacja wieku osoby wnioskującej o udzielenie pożyczki.""" + + def passes(self, person: Person) -> VerificationResult: + age = person.age + if age < 0: + logger.warning("Age is negative") + raise ValueError("Age cannot be negative!") + logger.info("Person has age %s", age) + result = 18 <= age <= 99 + return VerificationResult(self.name, result) + + @property + def name(self) -> str: + return "age" diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/__init__.py b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/identification_number.py b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/identification_number.py new file mode 100644 index 0000000..e8c990b --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/identification_number.py @@ -0,0 +1,26 @@ +import logging + +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification_result import VerificationResult +from smarttesting.verifier.verification import Verification + +logger = logging.getLogger(__name__) + + +class IdentificationNumberVerification(Verification): + """Weryfikacja poprawności numeru PESEL.""" + + def passes(self, person: Person) -> VerificationResult: + result = self._gender_matches_id_number(person) + return VerificationResult(self.name, result) + + @property + def name(self) -> str: + return "id" + + def _gender_matches_id_number(self, person: Person) -> bool: + tenth_character = person.national_id_number[9:10] + if int(tenth_character) % 2 == 0: + return person.gender == Gender.FEMALE + else: + return person.gender == Gender.MALE diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/name.py b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/name.py new file mode 100644 index 0000000..6b31591 --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification/name.py @@ -0,0 +1,15 @@ +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.verification_result import VerificationResult +from smarttesting.verifier.verification import Verification + + +class NameVerification(Verification): + """Weryfikacja po imieniu.""" + + def passes(self, person: Person) -> VerificationResult: + result = person.name is not None and person.name != "" + return VerificationResult(self.name, result) + + @property + def name(self) -> str: + return "name" diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification_listener.py b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification_listener.py new file mode 100644 index 0000000..a7f7e1d --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification_listener.py @@ -0,0 +1,31 @@ +import logging +from dataclasses import dataclass, field +from queue import Queue +from typing import List + +from smarttesting.verifier.application.verification_event import VerificationEvent + +logger = logging.getLogger(__name__) + + +@dataclass +class VerificationListener: + """Nasłuchiwacz na zdarzenia weryfikacyjne. Zapisuje je w kolejce.""" + + _queue: Queue = field(default_factory=Queue) + _events: List[VerificationEvent] = field(default_factory=list) + + def receive(self, event: VerificationEvent) -> None: + logger.info("Got an event! %s", event) + self._queue.put(event) + + def _drain(self) -> None: + """Pobierz nowe eventy i doczep je do listy w pamięci.""" + for _ in range(self._queue.qsize()): + event = self._queue.get_nowait() + self._events.append(event) + + @property + def received_events(self) -> List[VerificationEvent]: + self._drain() + return self._events[:] diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification_result.py b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification_result.py new file mode 100644 index 0000000..a2a9b6c --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/customer/verification_result.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class VerificationResult: + verification_name: str + result: bool diff --git a/08-advanced/08-04-mutation/smarttesting/verifier/verification.py b/08-advanced/08-04-mutation/smarttesting/verifier/verification.py new file mode 100644 index 0000000..5365e6b --- /dev/null +++ b/08-advanced/08-04-mutation/smarttesting/verifier/verification.py @@ -0,0 +1,15 @@ +import abc + +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.verification_result import VerificationResult + + +class Verification(abc.ABC): + @abc.abstractmethod + def passes(self, person: Person) -> VerificationResult: + pass + + @property + @abc.abstractmethod + def name(self) -> str: + pass diff --git a/08-advanced/08-04-mutation/tests/__init__.py b/08-advanced/08-04-mutation/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/tests/verifier/__init__.py b/08-advanced/08-04-mutation/tests/verifier/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/tests/verifier/customer/__init__.py b/08-advanced/08-04-mutation/tests/verifier/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/tests/verifier/customer/customer_verifier_tests.py b/08-advanced/08-04-mutation/tests/verifier/customer/customer_verifier_tests.py new file mode 100644 index 0000000..8171822 --- /dev/null +++ b/08-advanced/08-04-mutation/tests/verifier/customer/customer_verifier_tests.py @@ -0,0 +1,49 @@ +import uuid +from datetime import date + +import pytest +from smarttesting.customer.customer import Customer +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.customer_verifier import CustomerVerifier +from smarttesting.verifier.customer.verification_result import VerificationResult +from smarttesting.verifier.verification import Verification + + +class TestCustomerVerifier: + @pytest.fixture() + def too_young_stefan(self) -> Customer: + return Customer( + uuid.uuid4(), + Person("Stefan", "Stefanowski", date.today(), Gender.MALE, "1234567890"), + ) + + def test_should_collect_verification_results( + self, too_young_stefan: Customer + ) -> None: + verifications = {FirstVerification(), SecondVerification()} + verifier = CustomerVerifier(verifications) + + results = verifier.verify(too_young_stefan) + + assert set(results) == { + VerificationResult("first", False), + VerificationResult("second", True), + } + + +class FirstVerification(Verification): + def passes(self, person: Person) -> VerificationResult: + return VerificationResult(self.name, False) + + @property + def name(self) -> str: + return "first" + + +class SecondVerification(Verification): + def passes(self, person: Person) -> VerificationResult: + return VerificationResult(self.name, True) + + @property + def name(self) -> str: + return "second" diff --git a/08-advanced/08-04-mutation/tests/verifier/customer/verification/_02_age_verification_tests.py b/08-advanced/08-04-mutation/tests/verifier/customer/verification/_02_age_verification_tests.py new file mode 100644 index 0000000..1313994 --- /dev/null +++ b/08-advanced/08-04-mutation/tests/verifier/customer/verification/_02_age_verification_tests.py @@ -0,0 +1,128 @@ +from datetime import date + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification._01_age import AgeVerification + + +def zbigniew(age: int) -> Person: + today = date.today() + date_of_birth = today.replace(year=today.year - age) + return Person( + "Zbigniew", + "Stefanowski", + date_of_birth, + Gender.MALE, + "1234567890", + ) + + +class Test02AgeVerification: + @pytest.fixture() + def zbigniew_from_the_future(self) -> Person: + return zbigniew(age=-10) + + def test_should_throw_exception_when_age_invalid( + self, zbigniew_from_the_future: Person + ) -> None: + verification = AgeVerification() + + with pytest.raises(ValueError, match="Age cannot be negative!"): + verification.passes(zbigniew_from_the_future) + + @pytest.fixture() + def old_enough_zbigniew(self) -> Person: + return zbigniew(age=25) + + def test_should_return_positive_verification_when_age_is_within_the_threshold( + self, old_enough_zbigniew: Person + ) -> None: + verification = AgeVerification() + + result = verification.passes(old_enough_zbigniew) + + assert result.result is True + + @pytest.fixture() + def too_young_zbigniew(self) -> Person: + return zbigniew(age=0) + + def test_should_return_negative_verification_when_age_is_below_the_threshold( + self, too_young_zbigniew: Person + ) -> None: + verification = AgeVerification() + + result = verification.passes(too_young_zbigniew) + + assert result.result is False + + @pytest.fixture() + def too_old_zbigniew(self) -> Person: + return zbigniew(age=1000) + + def test_should_return_negative_verification_when_age_is_above_the_threshold( + self, too_old_zbigniew: Person + ) -> None: + verification = AgeVerification() + + result = verification.passes(too_old_zbigniew) + + assert result.result is False + + +@pytest.mark.skip +class Test02AgeVerificationBoundaryTests: + """Zakomentuj dekorator @pytest.mark.skip, żeby zwiększyć pokrycie kodu testami. + Pokrywamy warunki brzegowe! (i to z obu stron!)""" + + @pytest.fixture() + def lower_age_boundary_zbigniew(self) -> Person: + return zbigniew(age=18) + + def test_should_return_positive_verification_when_age_is_in_lower_boundary( + self, lower_age_boundary_zbigniew: Person + ) -> None: + verification = AgeVerification() + + result = verification.passes(lower_age_boundary_zbigniew) + + assert result.result is True + + @pytest.fixture() + def below_lower_age_boundary_zbigniew(self) -> Person: + return zbigniew(age=17) + + def test_should_return_negative_verification_when_age_is_below_lower_boundary( + self, below_lower_age_boundary_zbigniew: Person + ) -> None: + verification = AgeVerification() + + result = verification.passes(below_lower_age_boundary_zbigniew) + + assert result.result is False + + @pytest.fixture() + def upper_age_boundary_zbigniew(self) -> Person: + return zbigniew(age=99) + + def test_should_return_positive_verification_when_age_is_in_upper_boundary( + self, upper_age_boundary_zbigniew: Person + ) -> None: + verification = AgeVerification() + + result = verification.passes(upper_age_boundary_zbigniew) + + assert result.result is True + + @pytest.fixture() + def above_upper_age_boundary_zbigniew(self) -> Person: + return zbigniew(age=100) + + def test_should_return_negative_verification_when_age_is_above_upper_boundary( + self, above_upper_age_boundary_zbigniew: Person + ) -> None: + verification = AgeVerification() + + result = verification.passes(above_upper_age_boundary_zbigniew) + + assert result.result is False diff --git a/08-advanced/08-04-mutation/tests/verifier/customer/verification/__init__.py b/08-advanced/08-04-mutation/tests/verifier/customer/verification/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/08-advanced/08-04-mutation/tests/verifier/customer/verification/identification_number_verification_tests.py b/08-advanced/08-04-mutation/tests/verifier/customer/verification/identification_number_verification_tests.py new file mode 100644 index 0000000..213307e --- /dev/null +++ b/08-advanced/08-04-mutation/tests/verifier/customer/verification/identification_number_verification_tests.py @@ -0,0 +1,62 @@ +# pylint: disable=redefined-outer-name +from datetime import date + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.identification_number import ( + IdentificationNumberVerification, +) + + +@pytest.fixture() +def anna_the_woman() -> Person: + return Person("Anna", "Annowska", date.today(), Gender.FEMALE, "00000000020") + + +@pytest.fixture() +def anna_with_non_female_id() -> Person: + return Person("Anna", "Annowska", date.today(), Gender.FEMALE, "00000000010") + + +@pytest.fixture() +def zbigniew_the_man() -> Person: + return Person("Zbigniew", "Zbigniewowski", date.today(), Gender.MALE, "00000000010") + + +@pytest.fixture() +def zbigniew_with_non_male_id() -> Person: + return Person("Zbigniew", "Zbigniewowski", date.today(), Gender.MALE, "00000000020") + + +class TestIdentificationNumberVerification: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.verification = IdentificationNumberVerification() + + def test_should_return_positive_verification_when_gender_female_corresponds_to_id_number( + self, anna_the_woman: Person + ) -> None: + result = self.verification.passes(anna_the_woman) + + assert result.result is True + + def test_should_return_negative_verification_when_gender_female_does_not_correspond_to_id_number( + self, anna_with_non_female_id: Person + ) -> None: + result = self.verification.passes(anna_with_non_female_id) + + assert result.result is False + + def should_return_positive_verification_when_gender_male_corresponds_to_id_number( + self, zbigniew_the_man: Person + ) -> None: + result = self.verification.passes(zbigniew_the_man) + + assert result.result is True + + def test_should_return_negative_verification_when_gender_male_does_not_correspond_to_id_number( + self, zbigniew_with_non_male_id: Person + ) -> None: + result = self.verification.passes(zbigniew_with_non_male_id) + + assert result.result is False diff --git a/08-advanced/08-04-mutation/tests/verifier/customer/verification/name_verification_tests.py b/08-advanced/08-04-mutation/tests/verifier/customer/verification/name_verification_tests.py new file mode 100644 index 0000000..ecd3866 --- /dev/null +++ b/08-advanced/08-04-mutation/tests/verifier/customer/verification/name_verification_tests.py @@ -0,0 +1,20 @@ +from datetime import date + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.name import NameVerification + + +class TestNameVerification: + @pytest.fixture() + def nameless_person(self) -> Person: + return Person("", "Stefanowski", date.today(), Gender.MALE, "1234567890") + + def test_should_return_positive_result_when_name_is_not_blank( + self, nameless_person: Person + ) -> None: + verification = NameVerification() + + result = verification.passes(nameless_person) + + assert result.result is False diff --git a/08-advanced/08-04-mutation/tests/verifier/customer/verification_listener_tests.py b/08-advanced/08-04-mutation/tests/verifier/customer/verification_listener_tests.py new file mode 100644 index 0000000..d865925 --- /dev/null +++ b/08-advanced/08-04-mutation/tests/verifier/customer/verification_listener_tests.py @@ -0,0 +1,12 @@ +from smarttesting.verifier.application.verification_event import VerificationEvent +from smarttesting.verifier.customer.verification_listener import VerificationListener + + +class TestVerificationListener: + def test_should_add_event(self) -> None: + listener = VerificationListener() + event = VerificationEvent(self, "age", True) + + listener.receive(event) + + assert listener.received_events == [event]