From 047a7f457bb1aca4a994ec51b465d5e673f8f823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Buczyn=CC=81ski?= Date: Mon, 25 Sep 2023 17:38:14 +0200 Subject: [PATCH] Week 9 --- .../09-01-tdd/smarttesting/__init__.py | 0 .../09-01-tdd/tests/__init__.py | 0 .../09-01-tdd/tests/verifier/__init__.py | 0 .../verifier/tdd/_01_acceptance_tests.py | 26 +++ .../verifier/tdd/_02_acceptance_view_tests.py | 41 ++++ .../_03_acceptance_view_something_tests.py | 65 +++++++ .../tdd/_04_fraud_verifier_failing_tests.py | 73 +++++++ .../verifier/tdd/_05_fraud_verifier_tests.py | 54 ++++++ .../verifier/tdd/_06_acceptance_tests.py | 72 +++++++ .../09-01-tdd/tests/verifier/tdd/__init__.py | 0 .../09-02-pyramid/smarttesting/__init__.py | 0 .../09-02-pyramid/tests/__init__.py | 0 .../09-02-pyramid/tests/dummy_test.py | 2 + .../09-02-pyramid/tests/pyramid_tests.py | 38 ++++ .../09-03-bad-tests/smarttesting/__init__.py | 0 .../tests/_01_no_assertions_tests.py | 15 ++ .../_02_does_unittest_mock_work_tests.py | 33 ++++ .../_04_potential_fraud_service_tests.py | 109 +++++++++++ .../09-03-bad-tests/tests/__init__.py | 0 .../09-03-bad-tests/tests/bad_tests.py | 72 +++++++ .../smarttesting/__init__.py | 0 .../tests/__init__.py | 0 .../tests/badly_named_class_tests.py | 9 + .../tests/badly_named_file.py | 10 + .../tests/badly_named_tests.py | 13 ++ .../09-04-legacy/smarttesting/__init__.py | 0 .../09-04-legacy/tests/__init__.py | 0 .../tests/smarttesting/__init__.py | 0 .../passing_none/_15_fraud_verifier_tests.py | 86 +++++++++ .../smarttesting/passing_none/__init__.py | 0 .../seam/_02_fraud_verifier_tests.py | 179 ++++++++++++++++++ .../tests/smarttesting/seam/__init__.py | 0 .../smarttesting/simple/_01_fraud_verifier.py | 21 ++ .../tests/smarttesting/simple/__init__.py | 0 .../_08_special_tax_calculator_tax_test.py | 31 +++ .../tests/smarttesting/sprout/__init__.py | 0 .../fraud_tax_penalty_calculator_tests.py | 128 +++++++++++++ .../staticmethod/_17_fraud_verifier_tests.py | 50 +++++ .../smarttesting/staticmethod/__init__.py | 0 .../tests/smarttesting/staticmethod/client.py | 7 + .../smarttesting/staticmethod/other_module.py | 20 ++ 09-tests-and-design/09-homework/README.adoc | 6 + .../09-homework/smarttesting/__init__.py | 0 .../smarttesting/customer/__init__.py | 0 .../smarttesting/customer/customer.py | 28 +++ .../smarttesting/customer/person.py | 67 +++++++ .../09-homework/smarttesting/loan/__init__.py | 0 .../smarttesting/loan/loan_type.py | 6 + .../smarttesting/order/__init__.py | 0 .../smarttesting/order/loan_order.py | 29 +++ .../smarttesting/order/loan_order_service.py | 54 ++++++ .../smarttesting/order/promotion.py | 10 + .../smarttesting/verifier/__init__.py | 0 .../verifier/customer/__init__.py | 0 .../customer/customer_verification_result.py | 36 ++++ .../verifier/customer/customer_verifier.py | 22 +++ .../customer/verification/__init__.py | 0 .../verifier/customer/verification/age.py | 27 +++ .../business_rules_verification.py | 31 +++ .../verification/exception_raising_age.py | 12 ++ .../customer/verification/identity.py | 12 ++ .../verifier/customer/verification/name.py | 21 ++ .../verifier/customer/verification/surname.py | 15 ++ .../customer/verification/surname_checker.py | 8 + .../customer/verification/verifier_manager.py | 22 +++ .../smarttesting/verifier/event_emitter.py | 6 + .../smarttesting/verifier/verification.py | 9 + .../verifier/verification_event.py | 6 + .../09-homework/tests/__init__.py | 0 .../09-homework/tests/factories.py | 56 ++++++ .../tests/smarttesting/__init__.py | 0 .../tests/smarttesting/customer/__init__.py | 0 .../customer/person_done_tests.py | 49 +++++ .../smarttesting/customer/person_tests.py | 18 ++ .../tests/smarttesting/order/__init__.py | 0 .../order/loan_order_service_done_tests.py | 112 +++++++++++ .../order/loan_order_service_tests.py | 46 +++++ .../smarttesting/order/loan_order_tests.py | 19 ++ .../tests/smarttesting/verifier/__init__.py | 0 .../verifier/customer/__init__.py | 0 .../customer/customer_verifier_tests.py | 51 +++++ .../customer/verification/__init__.py | 0 .../customer/verification/age_done_tests.py | 50 +++++ .../customer/verification/age_tests.py | 21 ++ .../business_rules_verification_done_tests.py | 72 +++++++ .../business_rules_verification_tests.py | 74 ++++++++ .../exception_raising_age_done_tests.py | 40 ++++ .../exception_raising_age_tests.py | 29 +++ .../verification/identity_done_tests.py | 49 +++++ .../customer/verification/identity_tests.py | 21 ++ .../customer/verification/name_done_tests.py | 49 +++++ .../customer/verification/name_tests.py | 33 ++++ .../verification/surname_done_tests.py | 9 + .../customer/verification/surname_tests.py | 29 +++ 09-tests-and-design/README.adoc | 75 ++++++++ 95 files changed, 2483 insertions(+) create mode 100644 09-tests-and-design/09-01-tdd/smarttesting/__init__.py create mode 100644 09-tests-and-design/09-01-tdd/tests/__init__.py create mode 100644 09-tests-and-design/09-01-tdd/tests/verifier/__init__.py create mode 100644 09-tests-and-design/09-01-tdd/tests/verifier/tdd/_01_acceptance_tests.py create mode 100644 09-tests-and-design/09-01-tdd/tests/verifier/tdd/_02_acceptance_view_tests.py create mode 100644 09-tests-and-design/09-01-tdd/tests/verifier/tdd/_03_acceptance_view_something_tests.py create mode 100644 09-tests-and-design/09-01-tdd/tests/verifier/tdd/_04_fraud_verifier_failing_tests.py create mode 100644 09-tests-and-design/09-01-tdd/tests/verifier/tdd/_05_fraud_verifier_tests.py create mode 100644 09-tests-and-design/09-01-tdd/tests/verifier/tdd/_06_acceptance_tests.py create mode 100644 09-tests-and-design/09-01-tdd/tests/verifier/tdd/__init__.py create mode 100644 09-tests-and-design/09-02-pyramid/smarttesting/__init__.py create mode 100644 09-tests-and-design/09-02-pyramid/tests/__init__.py create mode 100644 09-tests-and-design/09-02-pyramid/tests/dummy_test.py create mode 100644 09-tests-and-design/09-02-pyramid/tests/pyramid_tests.py create mode 100644 09-tests-and-design/09-03-bad-tests/smarttesting/__init__.py create mode 100644 09-tests-and-design/09-03-bad-tests/tests/_01_no_assertions_tests.py create mode 100644 09-tests-and-design/09-03-bad-tests/tests/_02_does_unittest_mock_work_tests.py create mode 100644 09-tests-and-design/09-03-bad-tests/tests/_04_potential_fraud_service_tests.py create mode 100644 09-tests-and-design/09-03-bad-tests/tests/__init__.py create mode 100644 09-tests-and-design/09-03-bad-tests/tests/bad_tests.py create mode 100644 09-tests-and-design/09-03-discovery-problems/smarttesting/__init__.py create mode 100644 09-tests-and-design/09-03-discovery-problems/tests/__init__.py create mode 100644 09-tests-and-design/09-03-discovery-problems/tests/badly_named_class_tests.py create mode 100644 09-tests-and-design/09-03-discovery-problems/tests/badly_named_file.py create mode 100644 09-tests-and-design/09-03-discovery-problems/tests/badly_named_tests.py create mode 100644 09-tests-and-design/09-04-legacy/smarttesting/__init__.py create mode 100644 09-tests-and-design/09-04-legacy/tests/__init__.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/__init__.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/passing_none/_15_fraud_verifier_tests.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/passing_none/__init__.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/seam/_02_fraud_verifier_tests.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/seam/__init__.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/simple/_01_fraud_verifier.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/simple/__init__.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/_08_special_tax_calculator_tax_test.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/__init__.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/fraud_tax_penalty_calculator_tests.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/_17_fraud_verifier_tests.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/__init__.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/client.py create mode 100644 09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/other_module.py create mode 100644 09-tests-and-design/09-homework/README.adoc create mode 100644 09-tests-and-design/09-homework/smarttesting/__init__.py create mode 100644 09-tests-and-design/09-homework/smarttesting/customer/__init__.py create mode 100644 09-tests-and-design/09-homework/smarttesting/customer/customer.py create mode 100644 09-tests-and-design/09-homework/smarttesting/customer/person.py create mode 100644 09-tests-and-design/09-homework/smarttesting/loan/__init__.py create mode 100644 09-tests-and-design/09-homework/smarttesting/loan/loan_type.py create mode 100644 09-tests-and-design/09-homework/smarttesting/order/__init__.py create mode 100644 09-tests-and-design/09-homework/smarttesting/order/loan_order.py create mode 100644 09-tests-and-design/09-homework/smarttesting/order/loan_order_service.py create mode 100644 09-tests-and-design/09-homework/smarttesting/order/promotion.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/__init__.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/__init__.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/customer_verification_result.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/customer_verifier.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/__init__.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/age.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/business_rules_verification.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/exception_raising_age.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/identity.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/name.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/surname.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/surname_checker.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/verifier_manager.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/event_emitter.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/verification.py create mode 100644 09-tests-and-design/09-homework/smarttesting/verifier/verification_event.py create mode 100644 09-tests-and-design/09-homework/tests/__init__.py create mode 100644 09-tests-and-design/09-homework/tests/factories.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/__init__.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/customer/__init__.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/customer/person_done_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/customer/person_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/order/__init__.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_service_done_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_service_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/__init__.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/__init__.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/customer_verifier_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/__init__.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/age_done_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/age_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/business_rules_verification_done_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/business_rules_verification_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/exception_raising_age_done_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/exception_raising_age_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/identity_done_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/identity_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/name_done_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/name_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/surname_done_tests.py create mode 100644 09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/surname_tests.py create mode 100644 09-tests-and-design/README.adoc diff --git a/09-tests-and-design/09-01-tdd/smarttesting/__init__.py b/09-tests-and-design/09-01-tdd/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-01-tdd/tests/__init__.py b/09-tests-and-design/09-01-tdd/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-01-tdd/tests/verifier/__init__.py b/09-tests-and-design/09-01-tdd/tests/verifier/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_01_acceptance_tests.py b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_01_acceptance_tests.py new file mode 100644 index 0000000..9cbe4d7 --- /dev/null +++ b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_01_acceptance_tests.py @@ -0,0 +1,26 @@ +# pylint: disable=assignment-from-no-return +"""Wyłączamy czujnego pylinta, który oprotestowuje (słusznie!) niedokończony kod.""" +from typing import Any + +import pytest + + +class Test01Acceptance: + """Kod do slajdów [Zacznijmy od testu].""" + + @pytest.mark.xfail + def test_verifies_a_client_with_debt_as_fraud(self) -> None: + fraud = self._client_with_debt() + + verification = self._verify_client(fraud) + + self._assert_is_verified_as_fraud(verification) + + def _client_with_debt(self): + pass + + def _verify_client(self, client): + pass + + def _assert_is_verified_as_fraud(self, verification: Any): + assert verification is not None diff --git a/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_02_acceptance_view_tests.py b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_02_acceptance_view_tests.py new file mode 100644 index 0000000..2fbed94 --- /dev/null +++ b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_02_acceptance_view_tests.py @@ -0,0 +1,41 @@ +from enum import Enum +from typing import cast + +import pytest +from flask import Flask, Response + +app = Flask(__name__) + + +@app.route("/fraudCheck", methods=["POST"]) +def fraud_check(): + pass + + +class VerificationStatus(Enum): + FRAUD = "FRAUD" + NOT_FRAUD = "NOT_FRAUD" + + +class Test02AcceptanceView: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.client = app.test_client() + + @pytest.mark.xfail + def test_verifies_a_client_with_debt_as_fraud(self) -> None: + fraud = self._client_with_debt_payload() + + verification = self._verify_client(fraud) + + self._assert_is_verified_as_fraud(verification) + + def _client_with_debt_payload(self) -> dict: + return {"has_debt": True} + + def _verify_client(self, client_payload: dict) -> Response: + response = self.client.post("/fraudCheck", json=client_payload) + return cast(Response, response) + + def _assert_is_verified_as_fraud(self, verification: Response) -> None: + assert verification.json() == {"status": VerificationStatus.FRAUD.value} # type: ignore diff --git a/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_03_acceptance_view_something_tests.py b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_03_acceptance_view_something_tests.py new file mode 100644 index 0000000..c683afe --- /dev/null +++ b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_03_acceptance_view_something_tests.py @@ -0,0 +1,65 @@ +from enum import Enum +from typing import TypedDict, cast + +import injector +import pytest +from flask import Flask, Response, request +from flask_injector import FlaskInjector + + +class ClientPayload(TypedDict): + has_debt: bool + + +class Something: + def something(self, client: ClientPayload): + pass + + +class SomethingModule(injector.Module): + def configure(self, binder: injector.Binder) -> None: + binder.bind(Something, to=Something) + + +app = Flask(__name__) + + +@app.route("/fraudCheck", methods=["POST"]) +def fraud_check(something: Something): + payload = cast(ClientPayload, request.json) + result = something.something(payload) + return {"status": result.value} + + +# Musi być po skonfigurowaniu routingu +# https://github.com/alecthomas/flask_injector/issues/23#issuecomment-364600214 +FlaskInjector(app, [SomethingModule()]) + + +class VerificationStatus(Enum): + FRAUD = "FRAUD" + NOT_FRAUD = "NOT_FRAUD" + + +class Test03AcceptanceViewSomething: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.client = app.test_client() + + @pytest.mark.xfail + def test_verifies_a_client_with_debt_as_fraud(self) -> None: + fraud = self._client_with_debt_payload() + + verification = self._verify_client(fraud) + + self._assert_is_verified_as_fraud(verification) + + def _client_with_debt_payload(self) -> dict: + return {"has_debt": True} + + def _verify_client(self, client_payload: dict) -> Response: + response = self.client.post("/fraudCheck", json=client_payload) + return cast(Response, response) + + def _assert_is_verified_as_fraud(self, verification: Response): + assert verification.json == {"status": VerificationStatus.FRAUD.value} diff --git a/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_04_fraud_verifier_failing_tests.py b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_04_fraud_verifier_failing_tests.py new file mode 100644 index 0000000..ded278a --- /dev/null +++ b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_04_fraud_verifier_failing_tests.py @@ -0,0 +1,73 @@ +from enum import Enum +from typing import TypedDict, cast + +import injector +import pytest +from flask import Flask, Response, request +from flask_injector import FlaskInjector + + +class ClientPayload(TypedDict): + has_debt: bool + + +class FraudVerifier: + def verify(self, client: ClientPayload): + pass + + +class VerifierModule(injector.Module): + def configure(self, binder: injector.Binder) -> None: + binder.bind(FraudVerifier, to=FraudVerifier) + + +app = Flask(__name__) + + +@app.route("/fraudCheck", methods=["POST"]) +def fraud_check(verifier: FraudVerifier): + payload = cast(ClientPayload, request.json) + result = verifier.verify(payload) + return {"status": result.value} + + +# Musi być po skonfigurowaniu routingu +# https://github.com/alecthomas/flask_injector/issues/23#issuecomment-364600214 +FlaskInjector(app, [VerifierModule()]) + + +class VerificationStatus(Enum): + FRAUD = "FRAUD" + NOT_FRAUD = "NOT_FRAUD" + + +class TestFailingFraudVerifier: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.client = app.test_client() + + @pytest.mark.xfail + def test_verifies_a_client_with_debt_as_fraud(self) -> None: + fraud = self._client_with_debt_payload() + + verification = self._verify_client(fraud) + + assert verification.json == {"status": VerificationStatus.FRAUD.value} + + @pytest.mark.xfail + def test_verifies_a_client_without_debt_as_not_fraud(self) -> None: + fraud = self._client_without_debt_payload() + + verification = self._verify_client(fraud) + + assert verification.json == {"status": VerificationStatus.NOT_FRAUD.value} + + def _client_with_debt_payload(self) -> dict: + return {"has_debt": True} + + def _client_without_debt_payload(self) -> dict: + return {"has_debt": False} + + def _verify_client(self, client_payload: dict) -> Response: + response = self.client.post("/fraudCheck", json=client_payload) + return cast(Response, response) diff --git a/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_05_fraud_verifier_tests.py b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_05_fraud_verifier_tests.py new file mode 100644 index 0000000..4c75c4f --- /dev/null +++ b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_05_fraud_verifier_tests.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass +from enum import Enum +from typing import TypedDict + +import pytest + + +class ClientPayload(TypedDict): + has_debt: bool + + +class VerificationStatus(Enum): + FRAUD = "FRAUD" + NOT_FRAUD = "NOT_FRAUD" + + +@dataclass(frozen=True) +class VerificationResult: + status: VerificationStatus + + +class FraudVerifier: + def verify(self, client: ClientPayload) -> VerificationResult: + if client["has_debt"]: + return VerificationResult(VerificationStatus.FRAUD) + return VerificationResult(VerificationStatus.NOT_FRAUD) + + +class TestFraudVerifier: + def test_should_return_fraud_when_client_has_debt( + self, client_payload_with_debt: ClientPayload + ) -> None: + verifier = FraudVerifier() + + result = verifier.verify(client_payload_with_debt) + + assert result.status == VerificationStatus.FRAUD + + def test_should_return_not_fraud_when_client_has_no_debt( + self, client_payload_without_debt: ClientPayload + ) -> None: + verifier = FraudVerifier() + + result = verifier.verify(client_payload_without_debt) + + assert result.status == VerificationStatus.NOT_FRAUD + + @pytest.fixture() + def client_payload_with_debt(self) -> ClientPayload: + return {"has_debt": True} + + @pytest.fixture() + def client_payload_without_debt(self) -> ClientPayload: + return {"has_debt": False} diff --git a/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_06_acceptance_tests.py b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_06_acceptance_tests.py new file mode 100644 index 0000000..dc0698e --- /dev/null +++ b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/_06_acceptance_tests.py @@ -0,0 +1,72 @@ +from dataclasses import dataclass +from enum import Enum +from typing import TypedDict, cast + +import injector +import pytest +from flask import Flask, Response, request +from flask_injector import FlaskInjector + + +class ClientPayload(TypedDict): + has_debt: bool + + +class VerificationStatus(Enum): + FRAUD = "FRAUD" + NOT_FRAUD = "NOT_FRAUD" + + +@dataclass(frozen=True) +class VerificationResult: + status: VerificationStatus + + +class FraudVerifier: + def verify(self, client: ClientPayload) -> VerificationResult: + if client["has_debt"]: + return VerificationResult(VerificationStatus.FRAUD) + return VerificationResult(VerificationStatus.NOT_FRAUD) + + +class VerifierModule(injector.Module): + def configure(self, binder: injector.Binder) -> None: + binder.bind(FraudVerifier, to=FraudVerifier) + + +app = Flask(__name__) + + +@app.route("/fraudCheck", methods=["POST"]) +def fraud_check(verifier: FraudVerifier): + payload = cast(ClientPayload, request.json) + result = verifier.verify(payload) + return {"status": result.status.value} + + +# Musi być po skonfigurowaniu routingu +# https://github.com/alecthomas/flask_injector/issues/23#issuecomment-364600214 +FlaskInjector(app, [VerifierModule()]) + + +class Test06Acceptance: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.client = app.test_client() + + def test_verifies_a_client_with_debt_as_fraud(self) -> None: + fraud = self._client_with_debt_payload() + + verification = self._verify_client(fraud) + + self._assert_is_verified_as_fraud(verification) + + def _client_with_debt_payload(self) -> dict: + return {"has_debt": True} + + def _verify_client(self, client_payload: dict) -> Response: + response = self.client.post("/fraudCheck", json=client_payload) + return cast(Response, response) + + def _assert_is_verified_as_fraud(self, verification: Response): + assert verification.json == {"status": VerificationStatus.FRAUD.value} diff --git a/09-tests-and-design/09-01-tdd/tests/verifier/tdd/__init__.py b/09-tests-and-design/09-01-tdd/tests/verifier/tdd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-02-pyramid/smarttesting/__init__.py b/09-tests-and-design/09-02-pyramid/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-02-pyramid/tests/__init__.py b/09-tests-and-design/09-02-pyramid/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-02-pyramid/tests/dummy_test.py b/09-tests-and-design/09-02-pyramid/tests/dummy_test.py new file mode 100644 index 0000000..f7aca2e --- /dev/null +++ b/09-tests-and-design/09-02-pyramid/tests/dummy_test.py @@ -0,0 +1,2 @@ +def test_dummy(): + """Tylko by pytest nie narzekał na brak testów""" diff --git a/09-tests-and-design/09-02-pyramid/tests/pyramid_tests.py b/09-tests-and-design/09-02-pyramid/tests/pyramid_tests.py new file mode 100644 index 0000000..2d3e8e0 --- /dev/null +++ b/09-tests-and-design/09-02-pyramid/tests/pyramid_tests.py @@ -0,0 +1,38 @@ +# pylint: disable=unused-argument +"""Wyłączamy czujengo pylinta by nie oprotestowywał tego demonstracyjnego kodu.""" +from dataclasses import dataclass + +from flask import Flask + +app = Flask(__name__) + + +@app.route("/users/") +def user_view(repo: "UserRepository", user_id: str): + user = repo.find_by_id(user_id) + return serialize(user) + + +def serialize(user: "User") -> dict: + return {} + + +class User: + ... + + +class Dao: + def execute_sql(self, sql: str) -> User: + ... + + +class Repository: + ... + + +@dataclass +class UserRepository(Repository): + _dao: Dao + + def find_by_id(self, user_id: str) -> User: + return self._dao.execute_sql("...") diff --git a/09-tests-and-design/09-03-bad-tests/smarttesting/__init__.py b/09-tests-and-design/09-03-bad-tests/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-03-bad-tests/tests/_01_no_assertions_tests.py b/09-tests-and-design/09-03-bad-tests/tests/_01_no_assertions_tests.py new file mode 100644 index 0000000..78d4ab7 --- /dev/null +++ b/09-tests-and-design/09-03-bad-tests/tests/_01_no_assertions_tests.py @@ -0,0 +1,15 @@ +# pylint: disable=pointless-statement +"""Wyłączamy pylinta, który wyłapuje naszą demonstracyjną pomyłkę :)""" + + +class Test01NoAssertions: + def test_returns_sum_when_adding_two_numbers(self) -> None: + first_number = 1 + second_number = 2 + + # brakuje `assert`! + first_number + second_number == 3 + + def test_returns_sum_when_adding_two_numbers_fixed(self) -> None: + """Poprawiony test składający się z samej asercji.""" + assert 1 + 2 == 3 diff --git a/09-tests-and-design/09-03-bad-tests/tests/_02_does_unittest_mock_work_tests.py b/09-tests-and-design/09-03-bad-tests/tests/_02_does_unittest_mock_work_tests.py new file mode 100644 index 0000000..ce0f58a --- /dev/null +++ b/09-tests-and-design/09-03-bad-tests/tests/_02_does_unittest_mock_work_tests.py @@ -0,0 +1,33 @@ +import abc +from dataclasses import dataclass +from unittest.mock import Mock + + +class Test02DoesUnittestMockWork: + def test_returns_positive_fraud_verification_when_fraud(self) -> None: + """W tym teście de facto weryfikujemy czy framework do mockowania działa.""" + service = Mock(spec_set=AnotherFraudService) + service.is_fraud = Mock(return_value=True) + fraud_service = FraudService(service) + + result = fraud_service.check_if_fraud(Person()) + + assert result is True + + +class Person: + pass + + +class AnotherFraudService(abc.ABC): + @abc.abstractmethod + def is_fraud(self, person: Person) -> bool: + pass + + +@dataclass +class FraudService: + _service: AnotherFraudService + + def check_if_fraud(self, person: Person) -> bool: + return self._service.is_fraud(person) diff --git a/09-tests-and-design/09-03-bad-tests/tests/_04_potential_fraud_service_tests.py b/09-tests-and-design/09-03-bad-tests/tests/_04_potential_fraud_service_tests.py new file mode 100644 index 0000000..4c33d4d --- /dev/null +++ b/09-tests-and-design/09-03-bad-tests/tests/_04_potential_fraud_service_tests.py @@ -0,0 +1,109 @@ +"""Moduł z testami pokazująca jak stanowość może pokrzyżować nam plany w powtarzalnych +wynikach testów. + +Najpierw zakomentuj `@pytest.mark.skip` żeby wszystkie testy się uruchomiły. + +Następnie uruchom testy kilkukrotnie - zobaczysz, że czasami przechodzą, a czasami nie. +W czym problem? +""" +from dataclasses import dataclass +from typing import Dict, Optional + +import pytest + +pytest_plugins = ["pytest-randomly"] + + +@pytest.mark.skip +def test_counts_potential_frauds() -> None: + """Test ten oczekuje, że zawsze uruchomi się pierwszy. + + Dlatego oczekuje, że w cache'u będzie jeden wynik. Dla przypomnienia, cache jest + współdzielony przez wszystkie testy, ponieważ jest statyczny. + + W momencie uruchomienia testów w innej kolejności, inne testy też dodają wpisy + do cache'a. Zatem nie ma możliwości, żeby rozmiar cache'a wynosił 1. + """ + cache = PotentialFraudCache() + service = PotentialFraudService(cache) + + service.set_fraud("Kowalski") + + assert len(cache) == 1 + + +def test_sets_potential_fraud() -> None: + """Przykład testu, który weryfikuje czy udało nam się dodać wpis do cache'a. + + Zwiększa rozmiar cache'a o 1. Gdy ten test zostanie uruchomiony przed + `test_counts_potential_frauds` - wspomniany test się wywali. + """ + cache = PotentialFraudCache() + service = PotentialFraudService(cache) + + service.set_fraud("Oszustowski") + + assert cache.fraud("Oszustowski") is not None + + +def test_stores_potential_fraud() -> None: + """ + Potencjalne rozwiązanie problemu wspóldzielonego stanu. + + Najpierw zapisujemy stan wejściowy - jaki był rozmiar cache'a. + Dodajemy wpis do cache'a i sprawdzamy czy udało się go dodać i czy rozmiar jest + większy niż był. + + W przypadku uruchomienia wielu testów równolegle, sam fakt weryfikacji rozmiaru + jest niedostateczny, gdyż inny test mógł zwiększyć rozmiar cache'a. Koniecznym + jest zweryfikowanie, że istnieje w cache'u wpis dot. Kradzieja. + + BONUS: Jeśli inny test weryfikował usunięcie wpisu z cache'a, to asercja na rozmiar + może nam się wysypać. Należy rozważyć, czy nie jest wystarczającym zweryfikowanie + tylko obecności Kradzieja w cache'u! + """ + cache = PotentialFraudCache() + service = PotentialFraudService(cache) + initial_size = len(cache) + + service.set_fraud("Kradziej") + + assert len(cache) > initial_size + assert cache.fraud("Kradziej") is not None + + +@dataclass(frozen=True) +class PotentialFraud: + """Struktura reprezentująca potencjalnego oszusta.""" + + name: str + + +class PotentialFraudCache: + """Stan współdzielony między instancjami. Problemy? Np. nie zapewnimy wyłączności + dla pojedynczego wątku i dopuścimy odczyt/modyfikację danych przez wiele wątków + naraz. + + Przykładem może być aplikacja webowa z workerami-wątkami. + """ + + _cache: Dict[str, PotentialFraud] = {} + + def fraud(self, name: str) -> Optional[PotentialFraud]: + return self._cache.get(name) + + def put(self, fraud: PotentialFraud) -> None: + self._cache[fraud.name] = fraud + + def __len__(self) -> int: + return len(self._cache) + + +@dataclass +class PotentialFraudService: + """Serwis aplikacyjny opakowujący wywołania do cache'a.""" + + _cache: PotentialFraudCache + + def set_fraud(self, name: str) -> None: + self._cache.put(PotentialFraud(name)) diff --git a/09-tests-and-design/09-03-bad-tests/tests/__init__.py b/09-tests-and-design/09-03-bad-tests/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-03-bad-tests/tests/bad_tests.py b/09-tests-and-design/09-03-bad-tests/tests/bad_tests.py new file mode 100644 index 0000000..e919fb9 --- /dev/null +++ b/09-tests-and-design/09-03-bad-tests/tests/bad_tests.py @@ -0,0 +1,72 @@ +from dataclasses import dataclass +from typing import List +from unittest.mock import Mock, patch + + +def test_should_find_any_empty_name(): + """W tym teście mockujemy wszystko co się da. Włącznie z mockowaniem listy. + + Mockujemy nawet metodę __len__ z klasy str by taki zmockowany string był uznany + za "pusty". + + Nie przestrzegamy też podstawowej zasady higieny pracy z mockami - tj. używania + `spec` lub `spec_set`. + """ + name_mock = Mock(__len__=Mock(return_value=0)) + names_mock = Mock(__next__=Mock(side_effect=[name_mock, StopIteration])) + names_mock.__iter__ = Mock(return_value=names_mock) + + with patch.object(Dao, "store_in_db"): + assert _03_FraudService().any_name_is_empty(names_mock) is True + + +def test_should_find_any_empty_name_fixed(): + """Poprawiona wersja testu powyżej. + + Nie mockujemy listy - tworzymy ją. + Nie patchujemy też klasy używanej bezpośrednio. Raczej powinniśmy zmienić + design `_03_FraudService` by umożliwiała wstrzykiwanie zależności przez __init__, + jeżeli testowanie jej bez `Dao` jest pożądane. + """ + names = ["non empty", ""] + + assert _03_FraudService().any_name_is_empty(names) is True + + +def test_does_some_work_in_database_when_empty_string_found(): + """Przykład lepszego testu bez monkey-patching zbudowanego w oparciu o DI.""" + dao_mock = Mock(spec_set=Dao) + names = ["non empty", ""] + + FraudServiceFixed(dao_mock).any_name_is_empty(names) + + dao_mock.store_in_db.assert_called_once() + + +class _03_FraudService: # pylint: disable=invalid-name + """Klasa, w której korzystamy bezpośrednio z innych obiektów.""" + + def any_name_is_empty(self, names: List[str]) -> bool: + for name in names: + if not name: + Dao.store_in_db() + return True + return False + + +class Dao: + @classmethod + def store_in_db(cls) -> None: + pass + + +@dataclass +class FraudServiceFixed: + _dao: Dao + + def any_name_is_empty(self, names: List[str]) -> bool: + for name in names: + if not name: + self._dao.store_in_db() + return True + return False diff --git a/09-tests-and-design/09-03-discovery-problems/smarttesting/__init__.py b/09-tests-and-design/09-03-discovery-problems/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-03-discovery-problems/tests/__init__.py b/09-tests-and-design/09-03-discovery-problems/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-03-discovery-problems/tests/badly_named_class_tests.py b/09-tests-and-design/09-03-discovery-problems/tests/badly_named_class_tests.py new file mode 100644 index 0000000..d7461f0 --- /dev/null +++ b/09-tests-and-design/09-03-discovery-problems/tests/badly_named_class_tests.py @@ -0,0 +1,9 @@ +class BadlyNamedClassWithTests: + """Źle nazwana klasa uniemożliwiająca odkrycie jej przez pytesta. + + Przy obecnych ustawieniach, nazwa klasa powinna zaczynać się od słowa "Test". + Testowa metoda jest poprawnie nazwana, jednak nie zostanie "odkryta". + """ + + def test_passing(self) -> None: + assert True diff --git a/09-tests-and-design/09-03-discovery-problems/tests/badly_named_file.py b/09-tests-and-design/09-03-discovery-problems/tests/badly_named_file.py new file mode 100644 index 0000000..73acf62 --- /dev/null +++ b/09-tests-and-design/09-03-discovery-problems/tests/badly_named_file.py @@ -0,0 +1,10 @@ +"""Dobrze nazwane klasy testowe i testy, jednak plik o takiej nazwie jest ignorowany.""" + + +class TestDummy: + def test_passing(self) -> None: + assert True + + +def test_passes() -> None: + assert True diff --git a/09-tests-and-design/09-03-discovery-problems/tests/badly_named_tests.py b/09-tests-and-design/09-03-discovery-problems/tests/badly_named_tests.py new file mode 100644 index 0000000..7caaf92 --- /dev/null +++ b/09-tests-and-design/09-03-discovery-problems/tests/badly_named_tests.py @@ -0,0 +1,13 @@ +"""Źle nazwane testy chociaż klasa i plik sa nazwane poprawnie. + +Nazwa funkcji/metody testowej powinna zaczynać się słowem test_ +""" + + +class TestDummy: + def tst_passing(self) -> None: + assert True + + +def should_pass() -> None: + assert True diff --git a/09-tests-and-design/09-04-legacy/smarttesting/__init__.py b/09-tests-and-design/09-04-legacy/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-04-legacy/tests/__init__.py b/09-tests-and-design/09-04-legacy/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/__init__.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/passing_none/_15_fraud_verifier_tests.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/passing_none/_15_fraud_verifier_tests.py new file mode 100644 index 0000000..6a8edc8 --- /dev/null +++ b/09-tests-and-design/09-04-legacy/tests/smarttesting/passing_none/_15_fraud_verifier_tests.py @@ -0,0 +1,86 @@ +# pylint: disable=unused-argument,invalid-name +from dataclasses import dataclass + +import pytest + + +class Test15FraudVerifier: + def test_should_mark_client_with_debt_as_fraud(self) -> None: + """Przykład testu, gdzie zakładamy, że nie musimy tworzyć wszystkich obiektów + i podmieniamy je None'm. Jeśli zależność jest wymagana - test nam się wywali. + + Jest to ciut bezpieczniejsze niż Mock nienaśladujący żadnego obiektu. + """ + verifier = _16_FraudVerifier(None, None, DatabaseAccessor()) # type: ignore + + result = verifier.is_fraud("Fraudowski") + + assert result is True + + @pytest.mark.skip + def test_should_calculate_penalty_when_fraud_applies_for_a_loan(self) -> None: + """Przykład testu, gdzie zakładamy, że nie musimy tworzyć wszystkich obiektów + i podmieniamy je None'm. Niestety nie trafiamy i leci nam AttributeError, + gdyż dani kolaboratorzy byli wymagani. + """ + verifier = _16_FraudVerifier(PenaltyCalculator(), None, None) # type: ignore + + penalty = verifier.calculate_fraud_penalty("Fraudowski") + + assert penalty > 0 + + def test_should_calculate_penalty_when_fraud_applies_for_a_loan_with_both_deps( + self, + ) -> None: + """Wygląda na to, że musimy przekazać jeszcze `TaxHistoryRetriever`.""" + verifier = _16_FraudVerifier( + PenaltyCalculator(), TaxHistoryRetriever(), None # type: ignore + ) + + penalty = verifier.calculate_fraud_penalty("Fraudowski") + + assert penalty > 0 + + +@dataclass(frozen=True) +class Client: + name: str + has_debt: bool + + +class DatabaseAccessor: + def get_client_by_name(self, name: str) -> Client: + return Client("Fraudowski", True) + + +class PenaltyCalculator: + def calculate_penalty(self, client: Client) -> int: + return 100 + + +class TaxHistoryRetriever: + def return_last_revenue(self, client: Client) -> int: + return 150 + + +@dataclass +class _16_FraudVerifier: + """Implementacja zawierająca dużo zależności, skomplikowany, długi kod.""" + + _penalty: PenaltyCalculator + _history: TaxHistoryRetriever + _accessor: DatabaseAccessor + + def calculate_fraud_penalty(self, name: str) -> int: + # 5 000 linijek kodu dalej... + + # set client history to false, otherwise it won't work + last_revenue = self._history.return_last_revenue(Client(name, False)) + # set client history to true, otherwise it won't work + penalty = self._penalty.calculate_penalty(Client(name, True)) + return last_revenue // 50 + penalty + + def is_fraud(self, name: str) -> bool: + # 7 000 linijek kodu dalej.... + client = self._accessor.get_client_by_name(name) + return client.has_debt diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/passing_none/__init__.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/passing_none/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/seam/_02_fraud_verifier_tests.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/seam/_02_fraud_verifier_tests.py new file mode 100644 index 0000000..a5f8f63 --- /dev/null +++ b/09-tests-and-design/09-04-legacy/tests/smarttesting/seam/_02_fraud_verifier_tests.py @@ -0,0 +1,179 @@ +# pylint: disable=invalid-name,unused-argument +import abc +from dataclasses import dataclass +from typing import Optional +from unittest.mock import Mock + +import pytest + + +class Test02FraudVerifier: + @pytest.mark.skip + def test_marks_client_with_debt_as_fraud(self) -> None: + """Próba napisania testu do istniejącej klasy łączącej się z bazą danych.""" + accessor = _03_DatabaseAccessorImpl() + verifier = FraudVerifier(accessor) + + assert verifier.is_fraud("Fraudowski") is True + + def test_marks_client_with_debt_as_fraud_with_seam(self) -> None: + """Przykład testu z wykorzystaniem szwa (seam).""" + accessor = _04_FakeDatabaseAccessor() + verifier = FraudVerifier(accessor) + + assert verifier.is_fraud("Fraudowski") is True + + @pytest.mark.skip + def test_marks_client_with_debt_as_fraud_with_seam_logic_in_constructor( + self, + ) -> None: + verifier = _09_FraudVerifierLogicInConstructor() + + assert verifier.is_fraud("Fraudowski") is True + + @pytest.mark.skip + def test_creates_an_instance_of_fraud_verifier(self) -> None: + _09_FraudVerifierLogicInConstructor() + + def test_marks_client_with_debt_as_fraud_with_a_mock(self) -> None: + accessor = Mock(spec_set=_10_DatabaseAccessorImplWithLogicInTheConstructor) + client = Client(name="Fraudowski", has_debt=True) + accessor.get_client_by_name = Mock(return_value=client) + verifier = _11_FraudVerifierLogicInConstructorExtractLogic(accessor) + + assert verifier.is_fraud("Fraudowski") is True + + def test_marks_client_with_debt_as_fraud_with_an_extracted_interface(self) -> None: + accessor = _14_FakeDatabaseAccessorWithInterface() + verifier = _13_FraudVerifierWithInterface(accessor) + + assert verifier.is_fraud("Fraudowski") is True + + def test_marks_client_with_debt_as_fraud_with_seam_interface(self) -> None: + accessor = _14_FakeDatabaseAccessorWithInterface() + verifier = _13_FraudVerifierWithInterface(accessor) + + assert verifier.is_fraud("Fraudowski") is True + + +@dataclass(frozen=True) +class Client: + name: str + has_debt: bool + + +class _03_DatabaseAccessorImpl: + def get_client_by_name(self, name: str) -> Client: + client = self._perform_Long_running_task(name) + print(client.name) + self._do_some_additional_work(client) + return client + + def _perform_Long_running_task(self, name: str) -> Client: + raise IOError("Can't connect to the database!") + + def _do_some_additional_work(self, client: Client) -> None: + print("Additional work done") + + +class _04_FakeDatabaseAccessor(_03_DatabaseAccessorImpl): + """Nasz szew (seam)! + + Nadpisujemy problematyczną metodę bez zmiany kodu produkcyjnego. + """ + + def get_client_by_name(self, name: str) -> Client: + return Client(name="Fraudowski", has_debt=True) + + +class _12_DatabaseAccessor(abc.ABC): + @abc.abstractmethod + def get_client_by_name(self, name: str) -> Client: + pass + + +class _14_FakeDatabaseAccessorWithInterface(_12_DatabaseAccessor): + def get_client_by_name(self, name: str) -> Client: + return Client(name="Fraudowski", has_debt=True) + + +class DatabaseAccessorImplWithInterface(_12_DatabaseAccessor): + def __init__(self) -> None: + self._connect_to_db() + + def _connect_to_db(self) -> None: + raise IOError("Can't connect to the database!") + + def get_client_by_name(self, name: str) -> Client: + client = self._perform_Long_running_task(name) + print(client.name) + self._do_some_additional_work(client) + return client + + def _perform_Long_running_task(self, name: str) -> Client: + raise IOError("Can't connect to the database!") + + def _do_some_additional_work(self, client: Client) -> None: + print("Additional work done") + + +@dataclass +class _13_FraudVerifierWithInterface: + _accessor: _12_DatabaseAccessor + + def is_fraud(self, name: str) -> bool: + client = self._accessor.get_client_by_name(name) + return client.has_debt + + +@dataclass +class FraudVerifier: + _accessor: _03_DatabaseAccessorImpl + + def is_fraud(self, name: str) -> bool: + client = self._accessor.get_client_by_name(name) + return client.has_debt + + +class _10_DatabaseAccessorImplWithLogicInTheConstructor: + def __init__(self) -> None: + self._connect_to_db() + + def _connect_to_db(self) -> None: + raise IOError("Can't connect to the database!") + + def get_client_by_name(self, name: str) -> Client: + client = self._perform_Long_running_task(name) + print(client.name) + self._do_some_additional_work(client) + return client + + def _perform_Long_running_task(self, name: str) -> Client: + raise IOError("Can't connect to the database!") + + def _do_some_additional_work(self, client: Client) -> None: + print("Additional work done") + + +class _09_FraudVerifierLogicInConstructor: + def __init__(self) -> None: + self._accessor = _10_DatabaseAccessorImplWithLogicInTheConstructor() + + def is_fraud(self, name: str) -> bool: + client = self._accessor.get_client_by_name(name) + return client.has_debt + + +class _11_FraudVerifierLogicInConstructorExtractLogic: + def __init__( + self, + accessor: Optional[_10_DatabaseAccessorImplWithLogicInTheConstructor] = None, + ) -> None: + if accessor is None: # domyślne zachowanie + self._accessor = _10_DatabaseAccessorImplWithLogicInTheConstructor() + else: + self._accessor = accessor + + def is_fraud(self, name: str) -> bool: + client = self._accessor.get_client_by_name(name) + return client.has_debt diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/seam/__init__.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/seam/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/simple/_01_fraud_verifier.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/simple/_01_fraud_verifier.py new file mode 100644 index 0000000..0ca4499 --- /dev/null +++ b/09-tests-and-design/09-04-legacy/tests/smarttesting/simple/_01_fraud_verifier.py @@ -0,0 +1,21 @@ +# pylint: disable=invalid-name,unused-argument +from dataclasses import dataclass + + +@dataclass +class Client: + has_debt: bool = False + + +class DaoImpl: + def get_client_by_name(self, name: str) -> Client: + return Client() + + +@dataclass +class _01_FraudVerifier: + _dao_impl: DaoImpl + + def is_fraud(self, name: str) -> bool: + client = self._dao_impl.get_client_by_name(name) + return client.has_debt diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/simple/__init__.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/simple/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/_08_special_tax_calculator_tax_test.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/_08_special_tax_calculator_tax_test.py new file mode 100644 index 0000000..0adc444 --- /dev/null +++ b/09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/_08_special_tax_calculator_tax_test.py @@ -0,0 +1,31 @@ +class Test08SpecialTaxCalculator: + def test_does_not_apply_special_tax_when_amount_not_reaching_threshold( + self, + ) -> None: + initial_amount = 8 + calculator = SpecialTaxCalculator(initial_amount) + + result = calculator.calculate() + + assert result == initial_amount + + def test_applies_special_tax_when_amount_reaches_threshold(self) -> None: + initial_amount = 25 + calculator = SpecialTaxCalculator(initial_amount) + + result = calculator.calculate() + + assert result == 500 + + +class SpecialTaxCalculator: + AMOUNT_THRESHOLD = 10 + TAX_MULTIPLIER = 20 + + def __init__(self, amount: int) -> None: + self._amount = amount + + def calculate(self): + if self._amount <= self.AMOUNT_THRESHOLD: + return self._amount + return self._amount * self.TAX_MULTIPLIER diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/__init__.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/fraud_tax_penalty_calculator_tests.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/fraud_tax_penalty_calculator_tests.py new file mode 100644 index 0000000..ec6fb87 --- /dev/null +++ b/09-tests-and-design/09-04-legacy/tests/smarttesting/sprout/fraud_tax_penalty_calculator_tests.py @@ -0,0 +1,128 @@ +# pylint: disable=invalid-name +from dataclasses import dataclass + +from tests.smarttesting.sprout._08_special_tax_calculator_tax_test import ( + SpecialTaxCalculator, +) + + +class TestFraudTaxPenaltyCalculatorImpl: + def test_should_calculate_the_tax_for_fraudowski(self) -> None: + """Test z wykorzystaniem sztucznej implementacji dostępu do bazy danych.""" + fraudowski_amount = 100 + accessor = FakeDatabaseAccessorImpl(fraudowski_amount) + calculator = _05_FraudTaxPenaltyCalculator(accessor) + + tax = calculator.calculate_fraud_tax("Fraudowski") + + assert tax == fraudowski_amount * 100 + + def test_should_calculate_the_tax_for_fraudowski_with_if_else(self) -> None: + """Test z wykorzystaniem sztucznej implementacji dostępu do bazy danych. + + Weryfikuje implementację z użyciem if / else. + """ + fraudowski_amount = 100 + accessor = FakeDatabaseAccessorImpl(fraudowski_amount) + calculator = _06_FraudTaxPenaltyCalculatorIfElse(accessor) + + tax = calculator.calculate_fraud_tax("Fraudowski") + + assert tax == fraudowski_amount * 100 * 10 + + def test_should_calculate_the_tax_for_fraudowski_with_sprout(self) -> None: + """Test z wykorzystaniem sztucznej implementacji dostępu do bazy danych. + + Weryfikuje implementację z użyciem klasy kiełkującej. + """ + fraudowski_amount = 100 + accessor = FakeDatabaseAccessorImpl(fraudowski_amount) + calculator = _07_FraudTaxPenaltyCalculatorSprout(accessor) + + tax = calculator.calculate_fraud_tax("Fraudowski") + + assert tax == fraudowski_amount * 100 * 20 + + +@dataclass(frozen=True) +class Client: + name: str + has_debt: bool + amount: int + + +class DatabaseAccessorImpl: + def get_client_by_name(self, name: str) -> Client: + return Client(name, True, 100) + + +class FakeDatabaseAccessorImpl(DatabaseAccessorImpl): + def __init__(self, amount: int) -> None: + self._amount = amount + + def get_client_by_name(self, name: str) -> Client: + return Client("Fraudowski", True, self._amount) + + +@dataclass +class _05_FraudTaxPenaltyCalculator: + """Kalkulator podatku dla oszustów. Nie mamy do niego testów.""" + + _accessor: DatabaseAccessorImpl + + def calculate_fraud_tax(self, name: str) -> int: + client = self._accessor.get_client_by_name(name) + if client.amount < 0: + # WARNING: Don't touch this + # nobody knows why it should be -3 anymore + # but nothing works if you change this + return -3 + return self._calculate_tax(client.amount) + + def _calculate_tax(self, amount: int) -> int: + return amount * 100 + + +@dataclass +class _06_FraudTaxPenaltyCalculatorIfElse: + """Nowa funkcja systemu - dodajemy kod do nieprzetestowanego kodu.""" + + _accessor: DatabaseAccessorImpl + + def calculate_fraud_tax(self, name: str) -> int: + client = self._accessor.get_client_by_name(name) + if client.amount < 0: + # WARNING: Don't touch this + # nobody knows why it should be -3 anymore + # but nothing works if you change this + return -3 + tax = self._calculate_tax(client.amount) + if tax > 10: + return tax * 10 + return tax + + def _calculate_tax(self, amount: int) -> int: + return amount * 100 + + +@dataclass +class _07_FraudTaxPenaltyCalculatorSprout: + """Klasa kiełkowania (sprout). Wywołamy kod, który został przetestowany. + Piszemy go poprzez TDD. + """ + + _accessor: DatabaseAccessorImpl + + def calculate_fraud_tax(self, name: str) -> int: + client = self._accessor.get_client_by_name(name) + if client.amount < 0: + # WARNING: Don't touch this + # nobody knows why it should be -3 anymore + # but nothing works if you change this + return -3 + tax = self._calculate_tax(client.amount) + # chcemy obliczyć specjalny podatek + return SpecialTaxCalculator(tax).calculate() + + def _calculate_tax(self, amount: int) -> int: + return amount * 100 diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/_17_fraud_verifier_tests.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/_17_fraud_verifier_tests.py new file mode 100644 index 0000000..87d1ce5 --- /dev/null +++ b/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/_17_fraud_verifier_tests.py @@ -0,0 +1,50 @@ +# pylint: disable=invalid-name,protected-access +from unittest.mock import patch + +import pytest +from tests.smarttesting.staticmethod import other_module +from tests.smarttesting.staticmethod.client import Client + + +class Test17FraudVerifier: + @pytest.mark.skip + def test_should_mark_client_with_debt_as_fraud(self) -> None: + """Test się wywala, gdyż wywołanie `is_fraud` wywoła połączenie do bazy danych. + + Nie wierzysz? Odkomentuj @pytest.mark.skip i sprawdź sam! + """ + verifier = _18_FraudVerifier() + + result = verifier.is_fraud("Fraudowski") + + assert result is True + + def test_should_mark_client_with_debt_as_fraud_with_imported_from_other_module( + self, + ) -> None: + """Test wykorzystujący czarną magię monkey-patchingu do poradzenia sobie z + poleganiem na zmiennej importowanej z innego modułu przechowującej instancję. + + Swoisty najprostszy sposób na singleton w Pythonie. + """ + verifier = _18_FraudVerifier() + + # Zastępujemy "singletona" naszą instancją + new_obj = _21_FakeDatabaseAccessor() + with patch.object(other_module, "database_accessor", new=new_obj): + result = verifier.is_fraud("Fraudowski") + + assert result is True + + +class _18_FraudVerifier: + """Przykład implementacji wołającej instancję zainicjalizowaną w innym module.""" + + def is_fraud(self, name: str) -> bool: + client = other_module.database_accessor.get_client_by_name(name) + return client.has_debt + + +class _21_FakeDatabaseAccessor(other_module._19_DatabaseAccessor): + def get_client_by_name(self, name: str) -> Client: + return Client("Fraudowski", True) diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/__init__.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/client.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/client.py new file mode 100644 index 0000000..39abbff --- /dev/null +++ b/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/client.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Client: + name: str + has_debt: bool diff --git a/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/other_module.py b/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/other_module.py new file mode 100644 index 0000000..43d7757 --- /dev/null +++ b/09-tests-and-design/09-04-legacy/tests/smarttesting/staticmethod/other_module.py @@ -0,0 +1,20 @@ +# pylint: disable=invalid-name,unused-argument +from tests.smarttesting.staticmethod.client import Client + + +class _19_DatabaseAccessor: + def get_client_by_name(self, name: str) -> Client: + client = self._perform_long_running_task(name) + print(client.name) + self._do_some_additional_work(client) + return client + + def _perform_long_running_task(self, name: str) -> Client: + raise IOError("Can't connect to the database!") + + def _do_some_additional_work(self, client: Client) -> None: + print("Additional work done") + + +# Tak można mieć singleton w Pythonie 8) +database_accessor = _19_DatabaseAccessor() diff --git a/09-tests-and-design/09-homework/README.adoc b/09-tests-and-design/09-homework/README.adoc new file mode 100644 index 0000000..4def610 --- /dev/null +++ b/09-tests-and-design/09-homework/README.adoc @@ -0,0 +1,6 @@ +== Znajdywanie testów do poprawienia w ramach pracy domowej + +Uruchom testy opatrzone markerem `homework` +```bash +pytest 09-tests-and-design/09-homework/ -m homework -v +``` diff --git a/09-tests-and-design/09-homework/smarttesting/__init__.py b/09-tests-and-design/09-homework/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/smarttesting/customer/__init__.py b/09-tests-and-design/09-homework/smarttesting/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/smarttesting/customer/customer.py b/09-tests-and-design/09-homework/smarttesting/customer/customer.py new file mode 100644 index 0000000..e3abc18 --- /dev/null +++ b/09-tests-and-design/09-homework/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/09-tests-and-design/09-homework/smarttesting/customer/person.py b/09-tests-and-design/09-homework/smarttesting/customer/person.py new file mode 100644 index 0000000..e4ff22b --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/customer/person.py @@ -0,0 +1,67 @@ +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 = self._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 + + def _today(self) -> date: + return date.today() diff --git a/09-tests-and-design/09-homework/smarttesting/loan/__init__.py b/09-tests-and-design/09-homework/smarttesting/loan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/smarttesting/loan/loan_type.py b/09-tests-and-design/09-homework/smarttesting/loan/loan_type.py new file mode 100644 index 0000000..eead995 --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/loan/loan_type.py @@ -0,0 +1,6 @@ +import enum + + +class LoanType(enum.Enum): + STUDENT = "STUDENT" + REGULAR = "REGULAR" diff --git a/09-tests-and-design/09-homework/smarttesting/order/__init__.py b/09-tests-and-design/09-homework/smarttesting/order/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/smarttesting/order/loan_order.py b/09-tests-and-design/09-homework/smarttesting/order/loan_order.py new file mode 100644 index 0000000..61cf176 --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/order/loan_order.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass, field +from datetime import date +from decimal import Decimal +from typing import List, Optional +from uuid import UUID + +from smarttesting.customer.customer import Customer +from smarttesting.loan.loan_type import LoanType +from smarttesting.order.promotion import Promotion + + +@dataclass +class LoanOrder: + + _order_date: date + customer: Customer + type: Optional[LoanType] = None + amount: Optional[Decimal] = None + interest_rate: Optional[Decimal] = None + commission: Optional[Decimal] = None + promotions: List[Promotion] = field(default_factory=list) + + @property + def order_date(self) -> date: + return self._order_date + + def add_manager_discount(self, manager_uuid: UUID) -> None: + new_promo = Promotion(f"Manager Promo: {manager_uuid}", Decimal("50")) + self.promotions.append(new_promo) diff --git a/09-tests-and-design/09-homework/smarttesting/order/loan_order_service.py b/09-tests-and-design/09-homework/smarttesting/order/loan_order_service.py new file mode 100644 index 0000000..dfcf988 --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/order/loan_order_service.py @@ -0,0 +1,54 @@ +import abc +from dataclasses import dataclass +from datetime import date +from decimal import Decimal + +from smarttesting.customer.customer import Customer +from smarttesting.loan.loan_type import LoanType +from smarttesting.order.loan_order import LoanOrder +from smarttesting.order.promotion import Promotion + + +class PostgresDao(abc.ABC): + @abc.abstractmethod + def update_promotion_statistics(self, promotion_name: str) -> None: + pass + + @abc.abstractmethod + def update_promotion_discount( + self, promotion_name: str, new_discount: Decimal + ) -> None: + pass + + +class MongoDbDao(abc.ABC): + @abc.abstractmethod + def get_promotion_discount(self, promotion_name: str) -> Decimal: + pass + + +@dataclass +class LoanOrderService: + """Serwis procesujący przynawanie pożyczek. + + Działa w zależności od typu pożyczki i obowiązujących promocji. + """ + + _postgres_dao: PostgresDao + _mongo_db_dao: MongoDbDao + + def student_loan_order(self, customer: Customer) -> LoanOrder: + if not customer.is_student: + raise ValueError(f"Cannot order student loan, {customer} is not a student.") + + today = date.today() + loan_order = LoanOrder(today, customer) + loan_order.type = LoanType.STUDENT + discount = self._mongo_db_dao.get_promotion_discount("Student Promo") + loan_order.promotions.append(Promotion("Student Promo", discount)) + loan_order.commission = Decimal("200") + loan_order.amount = Decimal("500") + + self._postgres_dao.update_promotion_statistics("Student Promo") + + return loan_order diff --git a/09-tests-and-design/09-homework/smarttesting/order/promotion.py b/09-tests-and-design/09-homework/smarttesting/order/promotion.py new file mode 100644 index 0000000..fc2d001 --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/order/promotion.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass +from decimal import Decimal + + +@dataclass(frozen=True) +class Promotion: + """Reprezentuje promocję dla oferty pożyczek.""" + + name: str + discount: Decimal diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/__init__.py b/09-tests-and-design/09-homework/smarttesting/verifier/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/__init__.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/customer_verification_result.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/customer_verification_result.py new file mode 100644 index 0000000..3f3006f --- /dev/null +++ b/09-tests-and-design/09-homework/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/09-tests-and-design/09-homework/smarttesting/verifier/customer/customer_verifier.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/customer_verifier.py new file mode 100644 index 0000000..b9ca165 --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/customer/customer_verifier.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass +from typing import Set + +from smarttesting.customer.customer import Customer +from smarttesting.verifier.customer.customer_verification_result import ( + CustomerVerificationResult, +) +from smarttesting.verifier.verification import Verification + + +@dataclass +class CustomerVerifier: + _verifications: Set[Verification] + + def verify(self, customer: Customer) -> CustomerVerificationResult: + results = [ + verification.passes(customer.person) for verification in self._verifications + ] + if all(results): + return CustomerVerificationResult.create_passed(customer.uuid) + else: + return CustomerVerificationResult.create_failed(customer.uuid) diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/__init__.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/age.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/age.py new file mode 100644 index 0000000..550a481 --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/age.py @@ -0,0 +1,27 @@ +import logging +from dataclasses import dataclass + +from smarttesting.customer.person import Person +from smarttesting.verifier.event_emitter import EventEmitter +from smarttesting.verifier.verification import Verification +from smarttesting.verifier.verification_event import VerificationEvent + +logger = logging.getLogger(__name__) + + +@dataclass(unsafe_hash=True) +class AgeVerification(Verification): + """Weryfikacja wieku osoby wnioskującej o udzielenie pożyczki.""" + + _event_emitter: EventEmitter + + def passes(self, person: Person) -> bool: + age = person.age + if age < 0: + logger.warning("Age is negative") + self._event_emitter.emit(VerificationEvent(False)) + raise ValueError("Age cannot be negative!") + logger.info("Person has age %s", age) + result = 18 <= age <= 99 + self._event_emitter.emit(VerificationEvent(result)) + return result diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/business_rules_verification.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/business_rules_verification.py new file mode 100644 index 0000000..cc852fd --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/business_rules_verification.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass + +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.verification.verifier_manager import VerifierManager +from smarttesting.verifier.event_emitter import EventEmitter +from smarttesting.verifier.verification import Verification +from smarttesting.verifier.verification_event import VerificationEvent + + +@dataclass(unsafe_hash=True) +class BusinessRulesVerification(Verification): + """Weryfikacja po warunkach biznesowych. + + Chyba ta klasa robi za dużo, no ale trudno... + """ + + _event_emitter: EventEmitter + _verifier: VerifierManager + + def passes(self, person: Person) -> bool: + result = all( + [ + self._verifier.verify_name(person), + self._verifier.verify_address(person), + self._verifier.verify_phone(person), + self._verifier.verify_surname(person), + self._verifier.verify_tax_information(person), + ] + ) + self._event_emitter.emit(VerificationEvent(result)) + return result diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/exception_raising_age.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/exception_raising_age.py new file mode 100644 index 0000000..70c110e --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/exception_raising_age.py @@ -0,0 +1,12 @@ +from smarttesting.customer.person import Person +from smarttesting.verifier.verification import Verification + + +class ExceptionRaisingAgeVerification(Verification): + """Weryfikacja po wieku - jeśli nie przechodzi to leci wyjątek.""" + + def passes(self, person: Person) -> bool: + result = person.age >= 18 + if not result: + raise ValueError("You cannot be below 18 years old!") + return result diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/identity.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/identity.py new file mode 100644 index 0000000..cce52ab --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/identity.py @@ -0,0 +1,12 @@ +from smarttesting.customer.person import Person +from smarttesting.verifier.verification import Verification + + +class IdentityVerification(Verification): + """Weryfikacja po PESELu. + + TODO: Do zaimplementowania w najbliższym sprincie. + """ + + def passes(self, person: Person) -> bool: + return False diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/name.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/name.py new file mode 100644 index 0000000..371b0c1 --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/name.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass + +from smarttesting.customer.person import Person +from smarttesting.verifier.event_emitter import EventEmitter +from smarttesting.verifier.verification import Verification +from smarttesting.verifier.verification_event import VerificationEvent + + +@dataclass(unsafe_hash=True) +class NameVerification(Verification): + """Weryfikacja po imieniu.""" + + _event_emitter: EventEmitter + + def passes(self, person: Person) -> bool: + result = self._verify(person.name) + self._event_emitter.emit(VerificationEvent(result)) + return result + + def _verify(self, name: str) -> bool: + return name.isalpha() diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/surname.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/surname.py new file mode 100644 index 0000000..3bd5ce8 --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/surname.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.verification.surname_checker import SurnameChecker +from smarttesting.verifier.verification import Verification + + +@dataclass(unsafe_hash=True) +class SurnameVerification(Verification): + """Weryfikacja wieku osoby wnioskującej o udzielenie pożyczki.""" + + _surname_checker: SurnameChecker + + def passes(self, person: Person) -> bool: + return self._surname_checker.check_surname(person) diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/surname_checker.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/surname_checker.py new file mode 100644 index 0000000..40b2c2c --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/surname_checker.py @@ -0,0 +1,8 @@ +from smarttesting.customer.person import Person + + +class SurnameChecker: + """Klasa udająca, że weryfikuje osobę po nazwisku.""" + + def check_surname(self, person: Person) -> bool: # pylint: disable=unused-argument + return False diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/verifier_manager.py b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/verifier_manager.py new file mode 100644 index 0000000..7a30ef1 --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/customer/verification/verifier_manager.py @@ -0,0 +1,22 @@ +from smarttesting.customer.person import Person + + +class VerifierManager: + """Klasa udająca klasę, która robi zdecydowanie za dużo.""" + + def verify_tax_information( # pylint: disable=unused-argument + self, person: Person + ) -> bool: + return True + + def verify_address(self, person: Person) -> bool: # pylint: disable=unused-argument + return True + + def verify_name(self, person: Person) -> bool: # pylint: disable=unused-argument + return True + + def verify_surname(self, person: Person) -> bool: # pylint: disable=unused-argument + return True + + def verify_phone(self, person: Person) -> bool: # pylint: disable=unused-argument + return True diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/event_emitter.py b/09-tests-and-design/09-homework/smarttesting/verifier/event_emitter.py new file mode 100644 index 0000000..0653fbd --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/event_emitter.py @@ -0,0 +1,6 @@ +from smarttesting.verifier.verification_event import VerificationEvent + + +class EventEmitter: + def emit(self, event: VerificationEvent) -> None: + pass diff --git a/09-tests-and-design/09-homework/smarttesting/verifier/verification.py b/09-tests-and-design/09-homework/smarttesting/verifier/verification.py new file mode 100644 index 0000000..fb44e92 --- /dev/null +++ b/09-tests-and-design/09-homework/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/09-tests-and-design/09-homework/smarttesting/verifier/verification_event.py b/09-tests-and-design/09-homework/smarttesting/verifier/verification_event.py new file mode 100644 index 0000000..c241d6d --- /dev/null +++ b/09-tests-and-design/09-homework/smarttesting/verifier/verification_event.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class VerificationEvent: + passed: bool diff --git a/09-tests-and-design/09-homework/tests/__init__.py b/09-tests-and-design/09-homework/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/tests/factories.py b/09-tests-and-design/09-homework/tests/factories.py new file mode 100644 index 0000000..0610aab --- /dev/null +++ b/09-tests-and-design/09-homework/tests/factories.py @@ -0,0 +1,56 @@ +import uuid +from dataclasses import fields +from datetime import date +from typing import Any, Dict, Type + +import factory +from smarttesting.customer.customer import Customer +from smarttesting.customer.person import Gender, Person, Status + + +def _get_fields_mapping_for_dataclass(dataclass: Type) -> Dict[str, str]: + return { + field.name.lstrip("_"): field.name + for field in fields(dataclass) + if field.name.startswith("_") + } + + +def _get_date_of_birth_from_age(person_being_constructed: Any) -> date: + """Funkcja fabrykująca datę urodzenia zależnie od zadanego wieku. + + Nie będzie wywołana jeśli jawnie przekażemy `date_of_birth`. + """ + if getattr(person_being_constructed, "age", None) is None: + return date(1978, 9, 12) + + today = date.today() + return today.replace(year=today.year - person_being_constructed.age) + + +class PersonFactory(factory.Factory): + class Meta: + model = Person + rename = _get_fields_mapping_for_dataclass(Person) + + class Params: + student = False + age = None + + name = "Anna" + surname = "Kowalska" + date_of_birth = factory.LazyAttribute(_get_date_of_birth_from_age) + gender = Gender.FEMALE + national_id_number = "78091211463" + status = factory.LazyAttribute( + lambda o: Status.STUDENT if o.student else Status.NOT_STUDENT + ) + + +class CustomerFactory(factory.Factory): + class Meta: + model = Customer + rename = _get_fields_mapping_for_dataclass(Customer) + + uuid = factory.LazyFunction(uuid.uuid4) + person = factory.SubFactory(PersonFactory) diff --git a/09-tests-and-design/09-homework/tests/smarttesting/__init__.py b/09-tests-and-design/09-homework/tests/smarttesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/tests/smarttesting/customer/__init__.py b/09-tests-and-design/09-homework/tests/smarttesting/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/tests/smarttesting/customer/person_done_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/customer/person_done_tests.py new file mode 100644 index 0000000..19086ba --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/customer/person_done_tests.py @@ -0,0 +1,49 @@ +from datetime import date + +import pytest +from freezegun import freeze_time +from smarttesting.customer.person import Gender, Person + + +class TestPersonDone: + """ + Pierwotny test testował gettery i settery, które jedynie zwracały ustawioną wartość. + + To, co na pewno powinniśmy przetestować to sposób liczenia wieku - + tam nie jest zwracana ustawiona wartość wieku tylko jest on wyliczony. + """ + + def test_calculates_age_of_person(self) -> None: + """Przykład udanego wyliczenia wieku z wykorzystaniem klasy potomnej.""" + + class PersonUnderTest(Person): + def _today(self) -> date: + return date(2011, 11, 1) + + person = PersonUnderTest( + "name", "surname", date(2001, 11, 1), Gender.MALE, "1234567890" + ) + + assert person.age == 10 + + def test_calculates_age_of_person_using_freezegun(self) -> None: + """Przykład udanego wyliczenia wieku z wykorzystaniem freezeguna.""" + person = Person("name", "surname", date(2001, 11, 1), Gender.MALE, "1234567890") + + with freeze_time("2011-11-01"): + assert person.age == 10 + + def test_raises_attribute_error_when_date_invalid(self) -> None: + """Przykład wyliczenia wieku, który zakończy się rzuceniem wyjątku. + + Komentarz: W Pythonie z adnotacjami typów i mypy (lub innym narzędziem do ich + weryfikacji) jesteśmy bezpieczni przed tego typu błędami gdyż zostaniemy + ostrzeżeni. + """ + person = Person( + "name", "surname", None, Gender.MALE, "1234567890" # type: ignore + ) + + with pytest.raises(AttributeError): + # age to @property więc odwołanie się wywołuje przeliczenie + person.age # pylint: disable=pointless-statement diff --git a/09-tests-and-design/09-homework/tests/smarttesting/customer/person_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/customer/person_tests.py new file mode 100644 index 0000000..3ffaecb --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/customer/person_tests.py @@ -0,0 +1,18 @@ +from datetime import date + +import pytest +from smarttesting.customer.person import Gender, Person + + +class TestPerson: + @pytest.mark.homework( + reason="Zrefaktoruj ten test. Czy na pewno musimy tyle weryfikować?" + ) + def test_attributes_accessible(self) -> None: + person = Person("name", "surname", date(2001, 11, 1), Gender.MALE, "1234567890") + + assert person.name == "name" + assert person.surname == "surname" + assert person.gender == Gender.MALE + assert person.national_id_number == "1234567890" + assert person.age >= 9 diff --git a/09-tests-and-design/09-homework/tests/smarttesting/order/__init__.py b/09-tests-and-design/09-homework/tests/smarttesting/order/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_service_done_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_service_done_tests.py new file mode 100644 index 0000000..896e2ac --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_service_done_tests.py @@ -0,0 +1,112 @@ +import uuid +from dataclasses import dataclass +from datetime import date +from decimal import Decimal +from unittest.mock import Mock + +import pytest +from smarttesting.customer.customer import Customer +from smarttesting.customer.person import Gender, Person +from smarttesting.loan.loan_type import LoanType +from smarttesting.order.loan_order import LoanOrder +from smarttesting.order.loan_order_service import ( + LoanOrderService, + MongoDbDao, + PostgresDao, +) + + +class TestLoanOrderServiceDone: + """Pierwotny test jest mało czytelny. + + Po pierwsze nazwy metod są niedokładne, po drugie w możemy lepiej kod sformatować + i zastosować assert object, żeby zwiększyć czytelność sekcji then. + """ + + STUDENT_PROMO_DISCOUNT_NAME = "Student Promo" + DEFAULT_STUDENT_PROMO_DISCOUNT_VALUE = "100" + DEFAULT_STUDENT_PROMO_COMMISSION_VALUE = "200" + DEFAULT_STUDENT_PROMO_LOAN_AMOUNT = "500" + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self._postgres_dao = Mock(spec_set=PostgresDao) + self._mongo_dao = Mock(spec_set=MongoDbDao) + self._service = LoanOrderService(self._postgres_dao, self._mongo_dao) + + def test_raises_exception_when_not_a_student_wants_to_take_a_student_loan( + self, not_a_student: Customer + ) -> None: + with pytest.raises(ValueError, match="Cannot order student loan"): + self._service.student_loan_order(not_a_student) + + def test_grants_a_student_loan_when_a_student_applies_for_it( + self, student: Customer + ) -> None: + self._mongo_dao.get_promotion_discount = Mock( + return_value=Decimal(self.DEFAULT_STUDENT_PROMO_DISCOUNT_VALUE) + ) + + loan_order = self._service.student_loan_order(student) + + order_assert = self.LoanOrderAssert(loan_order) + order_assert.is_student_loan() + order_assert.has_student_promo_with_value( + self.DEFAULT_STUDENT_PROMO_DISCOUNT_VALUE + ) + order_assert.has_commission_equal_to( + self.DEFAULT_STUDENT_PROMO_COMMISSION_VALUE + ) + order_assert.has_amount_equal_to(self.DEFAULT_STUDENT_PROMO_LOAN_AMOUNT) + + @pytest.fixture() + def not_a_student(self) -> Customer: + return Customer( + uuid.uuid4(), Person("A", "B", date.today(), Gender.FEMALE, "1234567890") + ) + + @pytest.fixture() + def student(self, not_a_student: Customer) -> Customer: + not_a_student.student() + return not_a_student + + @dataclass(frozen=True) + class LoanOrderAssert: + """AssertObject. + + Klasę umieszczamy tutaj dla lepszej widoczności problemu. + """ + + _loan_order: LoanOrder + + def is_student_loan(self) -> None: + assert self._loan_order.type == LoanType.STUDENT + + def has_student_promo_with_value(self, value: str) -> None: + assert value + assert len(self._loan_order.promotions) == 1 + promo = self._loan_order.promotions[0] + expected_promo_name = TestLoanOrderServiceDone.STUDENT_PROMO_DISCOUNT_NAME + assert ( + promo.name == expected_promo_name + ), f"Promotion name should be {expected_promo_name} but was {promo.name}" + expected_value = Decimal(value) + assert ( + promo.discount == expected_value + ), f"Promotion value should be {expected_value} but was {promo.discount}" + + def has_commission_equal_to(self, value: str) -> None: + assert value + expected_commission = Decimal(value) + assert self._loan_order.commission == expected_commission, ( + f"Commission value should be {expected_commission} but is " + f"{self._loan_order.commission}" + ) + + def has_amount_equal_to(self, value: str) -> None: + assert value + expected_amount = Decimal(value) + assert self._loan_order.amount == expected_amount, ( + f"Commission value should be {expected_amount} but is " + f"{self._loan_order.amount}" + ) diff --git a/09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_service_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_service_tests.py new file mode 100644 index 0000000..0df1622 --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_service_tests.py @@ -0,0 +1,46 @@ +import uuid +from datetime import date +from decimal import Decimal +from unittest.mock import Mock + +import pytest +from smarttesting.customer.customer import Customer +from smarttesting.customer.person import Gender, Person +from smarttesting.loan.loan_type import LoanType +from smarttesting.order.loan_order_service import ( + LoanOrderService, + MongoDbDao, + PostgresDao, +) +from smarttesting.order.promotion import Promotion + + +@pytest.mark.homework(reason="Na pewno możemy popracować nad czytelnościa tych testów") +class TestLoanOrderService: + def test_not_a_student(self) -> None: + service = LoanOrderService(None, None) # type: ignore + + with pytest.raises(ValueError, match="Cannot order student loan"): + service.student_loan_order( + Customer( + uuid.uuid4(), + Person("A", "B", date.today(), Gender.FEMALE, "1234567890"), + ) + ) + + def test_student(self) -> None: + customer = Customer( + uuid.uuid4(), Person("A", "B", date.today(), Gender.FEMALE, "1234567890") + ) + customer.student() + mongodb_dao = Mock( + MongoDbDao, get_promotion_discount=Mock(return_value=Decimal("100")) + ) + service = LoanOrderService(Mock(PostgresDao), mongodb_dao) + student_loan_order = service.student_loan_order(customer) + assert student_loan_order.type == LoanType.STUDENT + assert student_loan_order.promotions[0] == Promotion( + "Student Promo", Decimal("100") + ) + assert student_loan_order.commission == Decimal("200") + assert student_loan_order.amount == Decimal("500") diff --git a/09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_tests.py new file mode 100644 index 0000000..d2ae802 --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/order/loan_order_tests.py @@ -0,0 +1,19 @@ +import uuid +from datetime import date +from decimal import Decimal + +from smarttesting.order.loan_order import LoanOrder +from tests.factories import CustomerFactory + + +class TestLoanOrder: + def test_adds_manager_promo(self) -> None: + loan_order = LoanOrder(date.today(), CustomerFactory.build()) + manager_uuid = uuid.uuid4() + + loan_order.add_manager_discount(manager_uuid) + + assert len(loan_order.promotions) == 1 + promo = loan_order.promotions[0] + assert str(manager_uuid) in promo.name + assert promo.discount == Decimal("50") diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/__init__.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/__init__.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/customer_verifier_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/customer_verifier_tests.py new file mode 100644 index 0000000..d6051bc --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/customer_verifier_tests.py @@ -0,0 +1,51 @@ +from datetime import date +from typing import Set +from unittest.mock import Mock, call + +import pytest +from smarttesting.customer.person import Gender +from smarttesting.verifier.customer.customer_verification_result import Status +from smarttesting.verifier.customer.customer_verifier import CustomerVerifier +from smarttesting.verifier.customer.verification.age import AgeVerification +from smarttesting.verifier.customer.verification.name import NameVerification +from smarttesting.verifier.event_emitter import EventEmitter +from smarttesting.verifier.verification import Verification +from smarttesting.verifier.verification_event import VerificationEvent +from tests.factories import CustomerFactory + + +class TestCustomerVerifier: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self._customer = CustomerFactory.build() + self._event_emitter = Mock(spec_set=EventEmitter) + verifications = self._build_verifications(self._event_emitter) + self._service = CustomerVerifier(verifications) + + def _build_verifications(self, event_emitter: EventEmitter) -> Set[Verification]: + return { + AgeVerification(event_emitter), + NameVerification(event_emitter), + } + + def test_verifies_person(self) -> None: + """Zastosowanie fabryki w setupie testu.""" + customer = CustomerFactory.build( + person__national_id_number="80030818293", + person__date_of_birth=date(1980, 3, 8), + person__gender=Gender.MALE, + ) + + result = self._service.verify(customer) + + assert result.status == Status.VERIFICATION_PASSED + assert result.user_id == customer.uuid + + def test_emits_verification_event(self) -> None: + self._service.verify(self._customer) + + # Weryfikacja interakcji. Sprawdzamy, że metoda emit zostala wywołana + # 3 razy z argumentem typu VerificationEvent o określonej wartości + self._event_emitter.emit.assert_has_calls( + [call(VerificationEvent(True)) for _ in range(2)] + ) diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/__init__.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/age_done_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/age_done_tests.py new file mode 100644 index 0000000..e6ae66c --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/age_done_tests.py @@ -0,0 +1,50 @@ +from datetime import date, timedelta +from unittest.mock import Mock + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.age import AgeVerification +from smarttesting.verifier.event_emitter import EventEmitter +from smarttesting.verifier.verification_event import VerificationEvent + + +class TestAgeVerificationDone: + """Pierwotny test ma dobry zamiary, ale wykonanie niestety takowe nie jest... + + Zamieniając w pierwotnym teście warunki weryfikacji mocka widzimy, że test dalej + przechodzi. Ponadto, okazuje się, że AttributeError może polecieć z `.age` (co ma + miejsce gdy przekazujemy None'a). + + Czyli powinniśmy sprawdzić wiadomość wyjątku i napisać dwa scenariusze testowe - + jeden dla None'a i jeden dla negatywnego wyniku. + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self._emitter_mock = Mock(spec_set=EventEmitter) + self._verification = AgeVerification(self._emitter_mock) + + def test_raises_exception_when_date_of_birth_not_set(self) -> None: + person = Person( + "jan", "kowalski", None, Gender.MALE, "abcdefghijkl" # type: ignore + ) + + with pytest.raises(AttributeError): + self._verification.passes(person) + + def test_emits_event_when_age_negative(self) -> None: + """Ustawiając datę na przyszłość uzyskujemy wartość ujemną wieku. + + W ten sposób jesteśmy w stanie zweryfikować, że emitter się wykonał.""" + person = Person( + "jan", + "kowalski", + date.today() + timedelta(days=5), + Gender.MALE, + "abcdefghijkl", + ) + + with pytest.raises(ValueError): + self._verification.passes(person) + + self._emitter_mock.emit.assert_called_once_with(VerificationEvent(False)) diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/age_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/age_tests.py new file mode 100644 index 0000000..e993e67 --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/age_tests.py @@ -0,0 +1,21 @@ +from unittest.mock import Mock + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.age import AgeVerification +from smarttesting.verifier.event_emitter import EventEmitter +from smarttesting.verifier.verification_event import VerificationEvent + + +@pytest.mark.homework(reason="Czy ten test na pewno weryfikuje... cokolwiek?") +class TestAgeVerification: + def test_emits_event_when_date_of_birth_invalid(self) -> None: + emitter = Mock(spec_set=EventEmitter) + verification = AgeVerification(emitter) + + with pytest.raises(AttributeError): + person = Person( + "jan", "kowalski", None, Gender.MALE, "abcdefghijkl" # type: ignore + ) + verification.passes(person) + emitter.emit.assert_called_once_with(VerificationEvent(False)) diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/business_rules_verification_done_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/business_rules_verification_done_tests.py new file mode 100644 index 0000000..b464e92 --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/business_rules_verification_done_tests.py @@ -0,0 +1,72 @@ +from datetime import date +from unittest.mock import Mock + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.business_rules_verification import ( + BusinessRulesVerification, +) +from smarttesting.verifier.customer.verification.verifier_manager import VerifierManager +from smarttesting.verifier.event_emitter import EventEmitter +from smarttesting.verifier.verification_event import VerificationEvent + + +class TestBusinessRulesVerificationDone: + """Pierwotny test był bardzo źle napisany. + + Nie dość, że nie wiemy, co testujemy patrząc na nazwę metody testowej, + to nawet nie wiemy gdzie jest sekcja when. Kod jest bardzo nieczytelny i robi + stanowczo za dużo. Używa też niepotrzebnych konkretnych weryfikacji. + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self._emitter = Mock(spec_set=EventEmitter) + self._manager = Mock( + spec_set=VerifierManager, + verify_name=Mock(return_value=True), + verify_surname=Mock(return_value=True), + verify_address=Mock(return_value=True), + verify_phone=Mock(return_value=True), + verify_tax_information=Mock(return_value=True), + ) + self._verification = BusinessRulesVerification(self._emitter, self._manager) + + @pytest.fixture() + def person(self) -> Person: + return Person("J", "K", date.today(), Gender.MALE, "1234567890") + + def test_passess_verification_when_all_verification_pass( + self, + person: Person, + ) -> None: + result = self._verification.passes(person) + + assert result is True + self._emitter.emit.assert_called_once_with(VerificationEvent(True)) + + @pytest.mark.parametrize( + "verification_to_fail", + [ + "verify_name", + "verify_surname", + "verify_address", + "verify_phone", + "verify_tax_information", + ], + ) + def test_fails_verification_when_a_single_verification_doesnt_pass( + self, + person: Person, + verification_to_fail: str, + ) -> None: + """Test parametryzowany przypadków negatywnych. + + Na podstawie typu weryfikacji ustawi stuba w odpowiednim stanie. + """ + setattr(self._manager, verification_to_fail, Mock(return_value=False)) + + result = self._verification.passes(person) + + assert result is False + self._emitter.emit.assert_called_once_with(VerificationEvent(False)) diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/business_rules_verification_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/business_rules_verification_tests.py new file mode 100644 index 0000000..946024f --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/business_rules_verification_tests.py @@ -0,0 +1,74 @@ +from datetime import date +from unittest.mock import Mock, patch + +import pytest +from smarttesting.customer.person import Person +from smarttesting.verifier.customer.verification.business_rules_verification import ( + BusinessRulesVerification, +) +from smarttesting.verifier.customer.verification.verifier_manager import VerifierManager +from smarttesting.verifier.event_emitter import EventEmitter + + +@pytest.mark.homework( + reason=( + "Czy ten test na pewno jest czytelny? Co on w ogóle testuje? " + "Czyżby wszystkie przypadki błędnych weryfikacji?" + ) +) +class TestBusinessRulesVerification: + def test(self) -> None: + emitter = Mock(spec_set=EventEmitter) + manager = Mock(spec_set=VerifierManager) + # Jan should fail + verify_name_patcher = patch.object( + manager, "verify_name", side_effect=lambda person: person.name != "Jan" + ) + verify_name_patcher.start() + business_rules_verification = BusinessRulesVerification(emitter, manager) + verify_address_patcher = patch.object( + manager, "verify_address", return_value=True + ) + verify_address_patcher.start() + verify_phone_patcher = patch.object(manager, "verify_phone", return_value=True) + verify_phone_patcher.start() + verify_tax_info_patcher = patch.object( + manager, "verify_tax_information", return_value=True + ) + verify_tax_info_patcher.start() + person = Person("Jan", "Kowalski", date.today(), None, "12309279124123") # type: ignore + verify_surname_patcher = patch.object( + manager, "verify_surname", return_value=True + ) + verify_surname_patcher.start() + passes = business_rules_verification.passes(person) + assert not passes + manager.verify_name.assert_called_once() + assert manager.verify_name.mock_calls[0][1][0].name == "Jan" + patch.stopall() + verify_name_patcher = patch.object(manager, "verify_name", return_value=True) + verify_name_patcher.start() + verify_address_patcher = patch.object( + manager, "verify_address", return_value=False + ) + verify_address_patcher.start() + passes = business_rules_verification.passes(person) + assert not passes + patch.stopall() + verify_address_patcher = patch.object( + manager, "verify_address", return_value=True + ) + verify_address_patcher.start() + verify_phone_patcher = patch.object(manager, "verify_phone", return_value=False) + verify_phone_patcher.start() + passes = business_rules_verification.passes(person) + assert not passes + patch.stopall() + verify_phone_patcher = patch.object(manager, "verify_phone", return_value=True) + verify_phone_patcher.start() + verify_tax_info_patcher = patch.object( + manager, "verify_tax_information", return_value=False + ) + verify_tax_info_patcher.start() + passes = business_rules_verification.passes(person) + assert not passes diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/exception_raising_age_done_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/exception_raising_age_done_tests.py new file mode 100644 index 0000000..9e55d8a --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/exception_raising_age_done_tests.py @@ -0,0 +1,40 @@ +from datetime import date + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.exception_raising_age import ( + ExceptionRaisingAgeVerification, +) + + +class TestExceptionRaisingAgeDone: + """Pierwotny test nie weryfikował nic albo nie dość dokładnie. + + Asercje były niedokończone; w przypadku pozytywnego scenariusza powinniśmy + sprawdzać czy wartość jest dokładnie True (operator is), a dla wyjątku użyć raczej + `pytest.raises` by test się załamał dla przypadku, gdy wyjątek nie jest rzucany. + + Ponadto test był nieczytelny.""" + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self._verification = ExceptionRaisingAgeVerification() + + def test_passes_when_a_person_is_an_adult(self, adult: Person) -> None: + result = self._verification.passes(adult) + + assert result is True + + def test_raises_exception_when_a_person_is_a_minor(self, minor: Person) -> None: + with pytest.raises(ValueError): + self._verification.passes(minor) + + @pytest.fixture() + def adult(self) -> Person: + today = date.today() + twenty_years_ago = today.replace(year=today.year - 20) + return Person("A", "B", twenty_years_ago, Gender.FEMALE, "34567890") + + @pytest.fixture() + def minor(self) -> Person: + return Person("A", "B", date.today(), Gender.FEMALE, "34567890") diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/exception_raising_age_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/exception_raising_age_tests.py new file mode 100644 index 0000000..a189e88 --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/exception_raising_age_tests.py @@ -0,0 +1,29 @@ +from datetime import date + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.exception_raising_age import ( + ExceptionRaisingAgeVerification, +) + + +@pytest.mark.homework("Czy na pewno te asercje są poprawne?") +class TestExceptionRaisingAge: + def test_good(self, good_person: Person) -> None: + assert ExceptionRaisingAgeVerification().passes(good_person) + + def test_bad(self, bad_person: Person) -> None: + try: + ExceptionRaisingAgeVerification().passes(bad_person) + except ValueError: + pass + + @pytest.fixture() + def good_person(self) -> Person: + today = date.today() + twenty_years_ago = today.replace(year=today.year - 20) + return Person("A", "B", twenty_years_ago, Gender.FEMALE, "34567890") + + @pytest.fixture() + def bad_person(self) -> Person: + return Person("A", "B", date.today(), Gender.FEMALE, "34567890") diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/identity_done_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/identity_done_tests.py new file mode 100644 index 0000000..c2fc2b1 --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/identity_done_tests.py @@ -0,0 +1,49 @@ +from datetime import date + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.identity import IdentityVerification + + +class TestIdentifyVerificationDone: + """Pierwotny test testuje tylko negatywny przypadek. + + Kod produkcyjny zawiera automatycznie generowany kod przez IDE. + Czasami zdarza się, że test przechodzi, a nie powinien tylko dlatego, że użyte + zostały wartości domyślne takie jak None, 0, False. + + W Pythonie na przykład można wpaść w pułapkę funkcji bez `return`, gdyż wtedy + wynikiem działania jest `None`. Jeżeli mamy tylko jeden test, który sprawdza czy + wynikiem jest właśnie `None` to przecież możemy dostać fałszywie przechodzący test + mimo np. braku implementacji. + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self._verification = IdentityVerification() + + def test_fails_for_an_invalid_identity_number( + self, person_with_invalid_pesel: Person + ) -> None: + result = self._verification.passes(person_with_invalid_pesel) + + assert result is False + + @pytest.mark.xfail( + reason="Teraz nie przejdzie, dopóki nie będzie dokończonej implementacji", + strict=True, + ) + def test_passes_for_a_valid_identity_number( + self, person_with_valid_pesel: Person + ) -> None: + result = self._verification.passes(person_with_valid_pesel) + + assert result is True + + @pytest.fixture() + def person_with_invalid_pesel(self) -> Person: + return Person("jan", "kowalski", date.today(), Gender.MALE, "abcdefghijk") + + @pytest.fixture() + def person_with_valid_pesel(self) -> Person: + return Person("jan", "kowalski", date.today(), Gender.MALE, "49120966834") diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/identity_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/identity_tests.py new file mode 100644 index 0000000..90c947f --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/identity_tests.py @@ -0,0 +1,21 @@ +from datetime import date + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.identity import IdentityVerification + + +@pytest.mark.homework("Czy ten test weryfikuje kod produkcyjny?") +class TestIdentifyVerification: + def test_fails_for_an_invalid_identity_number( + self, person_with_invalid_pesel: Person + ) -> None: + verification = IdentityVerification() + + result = verification.passes(person_with_invalid_pesel) + + assert result is False + + @pytest.fixture() + def person_with_invalid_pesel(self) -> Person: + return Person("jan", "kowalski", date.today(), Gender.MALE, "abcdefghijk") diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/name_done_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/name_done_tests.py new file mode 100644 index 0000000..7c5c878 --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/name_done_tests.py @@ -0,0 +1,49 @@ +from datetime import date + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.name import NameVerification +from smarttesting.verifier.event_emitter import EventEmitter + + +class TestNameVerificationDone: + """Pierwotny test duplikuje logikę weryfikacji w sekcji given. + + To oznacza, że de facto nic nie testujemy. Wykorzystujemy tę samą logikę do + przygotowania obiektu, który oczekujemy na wyjściu. Jeśli zmieni się logika + biznesowa oba nasze testy dalej będą przechodzić. + + Szczerze mówiąc to nawet nie weryfikujemy czy wynik boolowski jest true czy false. + Po prostu sprawdzamy czy jest taki sam jaki na wejściu. + """ + + @pytest.fixture(autouse=True) + def setup(self) -> None: + self._verification = NameVerification(EventEmitter()) + + def test_passes_when_name_is_alphanumeric( + self, person_with_valid_name: Person + ) -> None: + result = self._verification.passes(person_with_valid_name) + + assert result is True + + def test_fails_when_name_is_not_alphanumeric( + self, person_with_invalid_name: Person + ) -> None: + result = self._verification.passes(person_with_invalid_name) + + assert result is False + + @pytest.fixture() + def person_with_valid_name(self) -> Person: + return Person("jan", "kowalski", date.today(), Gender.MALE, "abcdefghijk") + + @pytest.fixture() + def person_with_invalid_name(self) -> Person: + """Często zdarza się tak, że jak obiekt podczas inicjalizacji potrzebuje kilku + parametrów tych samych typów. Warto jawnie wprowadzić nieprawidłową wartość w + każde inne "podejrzane" pole, tak żeby mieć pewność, że testujemy to, co + powinniśmy. + """ + return Person("", "Kowalski", date.today(), Gender.MALE, "abcdefghijk") diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/name_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/name_tests.py new file mode 100644 index 0000000..4c36c8c --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/name_tests.py @@ -0,0 +1,33 @@ +from datetime import date + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.name import NameVerification +from smarttesting.verifier.event_emitter import EventEmitter + + +@pytest.mark.homework("Czy ten test w ogóle coś testuje?") +class TestNameVerification: + def test_passes_when_name_is_alphanumeric( + self, person_with_valid_name: Person + ) -> None: + verification = NameVerification(EventEmitter()) + expected = verification.passes(person_with_valid_name) + + assert verification.passes(person_with_valid_name) == expected + + def test_fails_when_name_is_not_alphanumeric( + self, person_with_invalid_name: Person + ) -> None: + verification = NameVerification(EventEmitter()) + expected = verification.passes(person_with_invalid_name) + + assert verification.passes(person_with_invalid_name) == expected + + @pytest.fixture() + def person_with_valid_name(self) -> Person: + return Person("jan", "kowalski", date.today(), Gender.MALE, "abcdefghijk") + + @pytest.fixture() + def person_with_invalid_name(self) -> Person: + return Person("", "", date.today(), Gender.MALE, "abcdefghijk") diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/surname_done_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/surname_done_tests.py new file mode 100644 index 0000000..93fcbbb --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/surname_done_tests.py @@ -0,0 +1,9 @@ +class TestSurnameVerificationDone: + """Oryginalna wersja testu sprawdzała czy framework do mockowania działa. + + Ten test raczej nie ma sensu i albo jego funkcja powinna być testowana + testem wyższego rzędu. + Być może należałoby to przetestować w parze z SurnameChecker? + + Naszym zdaniem test w tej formie powinien być usunięty. + """ diff --git a/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/surname_tests.py b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/surname_tests.py new file mode 100644 index 0000000..4b8271b --- /dev/null +++ b/09-tests-and-design/09-homework/tests/smarttesting/verifier/customer/verification/surname_tests.py @@ -0,0 +1,29 @@ +from datetime import date +from unittest.mock import Mock + +import pytest +from smarttesting.customer.person import Gender, Person +from smarttesting.verifier.customer.verification.surname import SurnameVerification +from smarttesting.verifier.customer.verification.surname_checker import SurnameChecker + + +@pytest.mark.homework("Czy framework do mockowania działa?") +class TestSurnameVerification: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self._checker = Mock(spec_set=SurnameChecker) + self._verification = SurnameVerification(self._checker) + + def test_returns_false_when_surname_invalid(self, person: Person) -> None: + self._checker.check_surname = Mock(return_value=False) + + assert self._verification.passes(person) is False + + def test_returns_true_when_surname_invalid(self, person: Person) -> None: + self._checker.check_surname = Mock(return_value=True) + + assert self._verification.passes(person) is True + + @pytest.fixture() + def person(self) -> Person: + return Person("a", "b", date.today(), Gender.MALE, "1234567890") diff --git a/09-tests-and-design/README.adoc b/09-tests-and-design/README.adoc new file mode 100644 index 0000000..a1b881f --- /dev/null +++ b/09-tests-and-design/README.adoc @@ -0,0 +1,75 @@ += Testy A Design Aplikacji + +== Zacznijmy od testu! [01] + +=== Kod [09-01-tdd] + +Pythonowy odpowiednik kodu ze slajdu [Brak przyjemności z testowania] znajdziemy w +`05-architecture/05-02-packages/05-02-01-core/tests/lesson1/_01_bad_class_tests.py` - `Test01BadClass.test_heavy_monkey_patching`. + +Najpierw kodujemy w `tests/verifier/tdd/_01_acceptance_tests.py`. Na slajdach będziemy przechodzić linia po linii włącznie z kodem, który nie może zostać jeszcze zinterpretowany przez Pythona. + +Następnie kod, gdzie tworzymy widok, który nic nie robi jest dostępny w pliku `_02_acceptance_view_tests.py`. W tym momencie tworzymy prostą implementację widoku, który zwraca `None`. + +Potem w `_03_acceptance_view_something_tests.py` tworzymy klasę `Something`, która jeszcze nie do końca wiemy, co będzie robiła. + +Po tym, jak rozpiszemy sobie co mamy zrobić z naszym klientem, dochodzimy do wniosku, że chcemy zweryfikować oszusta. Zatem tworzymy klasę `FraudVerifier` w pliku `_04_fraud_verifier_failing_tests.py`, która jeszcze nie ma implementacji. + +W `_05_fraud_verifier_tests.py` zapisujemy przypadki testowe dla naszej implementacji weryfikacji oszusta. Najpierw chcemy żeby jeden test przeszedł, a potem drugi. + +W końcu możemy puścić zestaw testów akceptacyjnych `_06_acceptance_tests.py`, które teraz przechodzą. + +== Piramida testów [02] + +=== Kod [09-02-pyramid] + +Kod do slajdu dot. tego, czy piramida testów jest zawsze taka sama `pyramid_tests.py`. Pokazujemy tu symulacje widoku, który przekazuje wywołania do klasy łączącej się z bazą danych. + +== Przykłady złych testów [03] + +* Mockowanie typów wbudowanych. `09-03-bad-tests` zawiera plik `bad_tests.py` zawiera klasę `_03_FraudService`, gdzie korzystamy "na sztywno" z innej klasy - `Dao`. W teście `test_should_find_any_empty_name` mockujemy wszystko co się dai w dodatku stosujemy monkey-patching tak że całkowicie odseparowujemy klasę `_03_Fraud_Service`. +* Mockowanie typów wbudowanych c.d. - ten sam plik, tym razem test `test_should_find_any_empty_name_fixed`. Wiemy już, że monkey-patching na dłuższą metę jest szkodliwy. Tutaj pokazujemy jak przygotować nasze klasy na wstrzykiwanie zależności i że upraszcza to test. +* Brak asercji - Klasa `Test01NoAssertions` i zawsze przechodzący test. Pokazujemy problem pominięcia asercji skutkujący zawsze przechodzącymi testami (nawet jeśli testowana implementacja jest błędna). +* Za dużo mocków - Klasa `Test02DoesUnittestMockWork` i operowanie tylko na mockach. De facto nie testujemy nic, poza tym, że framework do mockowania działa. +* Stanowość - plik `_04_potential_fraud_service_tests.py` pokazuje problemy związane ze stanowością w testach. +* Testy niewykrywalne przez pytest poprzez nietrzymanie się ustawionej konwencji nazewniczych plików, klas i funkcji/metod testowych `09-03-discovery-problemts`. Warto odnotować, że pytest zwróci kod błędu 5, jeśli nie znajdzie żadnego testu. + +== Praca z zastanym kodem + +=== Kod [09-04-legacy] + +Klasa `_01_FraudVerifier` z pliku `_01_fraud_verifier.py` widoczna na slajdzie po [Cel pracy ze źle zaprojektowanym kodem]. + +W pliku `_02_fraud_verifier_tests.py` mamy klasę `_03_DatabaseAccessorImpl`. Na jej podstawie powstał kod na slajdzie po screenshocie 4 000 linii kodu. + +Następnie próba napisania testu `test_marks_client_with_debt_as_fraud`. + +Czas na szew (seam) - `_04_FakeDatabaseAccessor`. Nadpisujemy problematyczną metodę bez zmiany kodu produkcyjnego i test `test_marks_client_with_debt_as_fraud_with_seam`. + +Teraz chcemy dodać nową funkcję systemu do klasy `_05_FraudTaxPenaltyCalculator`. + +Pierwsze podejście z `if/else` w `_06_FraudTaxPenaltyCalculatorIfElse`. Problem w tym, że dodajemy nowy kod do nieprzetestowanego. + +Wprowadzamy pojęcie Klasy Kiełkowania (Sprout). Czyli za pomocą TDD piszemy nową, przetestowaną klasę, który wywołamy w naszym kodzie legacy (`_07_FraudTaxPenaltyCalculatorSprout`). Process TDD widoczny tu `_08_special_tax_calculator_tax_test.py`. + +Załóżmy, że mamy klasę, która wylicza czy dana osoba jest oszustem lub nie, w zależności od tego, czy posiada dług. By wyciągnąć te informacje, musimy odpytać bazę danych. Akcesor do bazy danych tworzony jest w konstruktorze. Załóżmy, że mamy taką implementację weryfikatora oszustów `_09_FraudVerifierLogicInConstructor` i taką dostępu do bazy danych `_10_DatabaseAccessorImplWithLogicInTheConstructor`. Pierwszą rzeczą, którą możemy zrobić to spróbować w ogóle utworzyć nasz obiekt. Napiszmy test `Test02FraudVerifier.test_creates_an_instance_of_fraud_verifier`. Test wybuchnie! Co możemy zrobić? + +W `_11_FraudVerifierLogicInConstructorExtractLogic` widzimy, że możemy dodać dodatkowy, domyślny argument initializera, żeby nie tworzyć problematycznego obiektu w środku, tylko opcjonalnie przekazać już utworzony wcześniej obiekt do initializera. Teraz, możemy utworzyc mocka problematycznego obiektu i napisać test `Test02FraudVerifier.test_marks_client_with_debt_as_fraud_with_a_mock`. + +Teraz możemy wprowadzić nową klasę abstrakcyjną `_12_DatabaseAccessor`, który pokrywa się z już istniejącym kodem. Podmieniamy w initializerze `FraudVerifier`a klasę konkretną na abstrakcyjną (`_13_FraudVerifierWithInterface`). Dzięki temu możemy też stworzyć implementację test-double na potrzeby testu `_14_FakeDatabaseAccessorWithInterface`. + +Poprzez taką operację jesteśmy w stanie bardzo uprościć nasz test `Test02FraudVerifier.test_marks_client_with_debt_as_fraud_with_an_extracted_interface`. + +==== Obiektu nie da się łatwo utworzyć + +Plik `_015_fraud_verifier_tests.py`. Zawiera implementację `_16_FraudVerifier` jako przykład implementacji z wieloma zależnościami i dużą liczbą linijek kodu. + +Pokazujemy dwa przykłady testów, w których próbujemy odgadnąć, które zależności są wymagane poprzez podstawienie None'a. `Test15FraudVerifier.test_should_calculate_penalty_when_fraud_applies_for_a_loan` nie trafiamy i leci `AttributeError`. W `Test15FraudVerifier.test_should_mark_client_with_debt_as_fraud` trafiamy i test nam przechodzi. W teście `Test15FraudVerifier.test_should_calculate_penalty_when_fraud_applies_for_a_loan_with_both_deps` przekazujemy brakującą zależność i test przechodzi. + +==== Globalne zależności + +Plik `_17_fraud_verifier_tests.py`. W klasie `_18_FraudVerifier` mamy przykład implementacji wołającej obiekt utworzony w innym module w miejscu i stamtąd zaimportowany - instancję `_19_DatabaseAccessor`. + +Ta sytuacja jest tą ostateczną, w której monkey-patching (`unittest.mock.patch`) nie jest najgorszym pomysłem. Za pomocą tej techniki podmieniamy na czas wywołania metody instancję w innym module. Żeby zachować resztki pozorów, użyjemy klasy dziedziczącej po tej kłopotliwej - `_21_FakeDatabaseAccessor`. Można by też użyć w tym miejscu Mocka (koniecznie ze `spec_set=_19_DatabaseAccessor`!). + +Dzięki używaniu context managera (słowo kluczowe `with`) z `patch` upewniamy się, że posprzątamy po sobie. Skorzystanie z innego API i ręczne patchowanie jest niezalecane ze względu na ryzyko niewyczyszczenia stanu (o ile programista zapomni, rzecz jasna) i powstanie bardzo trudnych do wyśledzenia błędów.