From b8336514e02d9e0d8d0c0d28edf315dee4235c35 Mon Sep 17 00:00:00 2001 From: Mike Fiedler Date: Tue, 22 Apr 2025 16:14:53 -0400 Subject: [PATCH 1/4] chore(deps): add pytest-mock See https://pytest-mock.readthedocs.io/ Signed-off-by: Mike Fiedler --- requirements/tests.in | 1 + requirements/tests.txt | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/requirements/tests.in b/requirements/tests.in index 0db91b605dc5..0c8c524f062c 100644 --- a/requirements/tests.in +++ b/requirements/tests.in @@ -4,6 +4,7 @@ freezegun pretend pytest>=3.0.0 pytest-icdiff +pytest-mock pytest-postgresql>=3.1.3,<8.0.0 pytest-randomly pytest-socket diff --git a/requirements/tests.txt b/requirements/tests.txt index 110aa87c7be1..4c6e6020e38c 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -252,6 +252,7 @@ pytest==8.3.5 \ # via # -r requirements/tests.in # pytest-icdiff + # pytest-mock # pytest-postgresql # pytest-randomly # pytest-socket @@ -261,6 +262,10 @@ pytest-icdiff==0.9 \ --hash=sha256:13aede616202e57fcc882568b64589002ef85438046f012ac30a8d959dac8b75 \ --hash=sha256:efee0da3bd1b24ef2d923751c5c547fbb8df0a46795553fba08ef57c3ca03d82 # via -r requirements/tests.in +pytest-mock==3.14.0 \ + --hash=sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f \ + --hash=sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0 + # via -r requirements/tests.in pytest-postgresql==7.0.1 \ --hash=sha256:7723dfbfc57ea6f6f9876c2828e7b36f8b0e60b6cb040b1ddd444a60eed06e0a \ --hash=sha256:cbc6a67bbad5128b1f00def8cca5cf597020acc79893723f7a9cb60981b6840f From 79f07585340c6ec26da04e9cebef8bae9cf44abf Mon Sep 17 00:00:00 2001 From: Mike Fiedler Date: Tue, 22 Apr 2025 16:15:24 -0400 Subject: [PATCH 2/4] test: use mocker to spy on service call Signed-off-by: Mike Fiedler --- tests/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c16f1137339e..d8c9310704cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -550,8 +550,10 @@ def search_service(): @pytest.fixture -def domain_status_service(): - return account_services.NullDomainStatusService() +def domain_status_service(mocker): + service = account_services.NullDomainStatusService() + mocker.spy(service, "get_domain_status") + return service class QueryRecorder: From c604fa8e6261cda6e2f266d037afd4364eba7236 Mon Sep 17 00:00:00 2001 From: Mike Fiedler Date: Tue, 22 Apr 2025 16:16:00 -0400 Subject: [PATCH 3/4] test: example of using service-with-spy Most of these are not necessary, but used to demonstrate the utility Signed-off-by: Mike Fiedler --- tests/unit/admin/views/test_users.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/unit/admin/views/test_users.py b/tests/unit/admin/views/test_users.py index bf9cf89a2364..1853aa4e1c81 100644 --- a/tests/unit/admin/views/test_users.py +++ b/tests/unit/admin/views/test_users.py @@ -1542,7 +1542,7 @@ def test_no_recovery_codes_provided(self, db_request, monkeypatch, user_service) class TestUserEmailDomainCheck: - def test_user_email_domain_check(self, db_request): + def test_user_email_domain_check(self, db_request, domain_status_service): user = UserFactory.create(with_verified_primary_email=True) db_request.POST["email_address"] = user.primary_email.email db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/foobar") @@ -1562,3 +1562,12 @@ def test_user_email_domain_check(self, db_request): ] assert user.primary_email.domain_last_checked is not None assert user.primary_email.domain_last_status == ["active"] + + # Total calls + assert domain_status_service.get_domain_status.call_count == 1 + # most recent return value + assert domain_status_service.get_domain_status.spy_return == ["active"] + # all return values + assert domain_status_service.get_domain_status.spy_return_list == [["active"]] + # most recent exception + assert domain_status_service.get_domain_status.spy_exception is None From 0fe3b91e4bf54ed40b23b32d03a6bdd5aa73cd7d Mon Sep 17 00:00:00 2001 From: Mike Fiedler Date: Tue, 22 Apr 2025 16:34:20 -0400 Subject: [PATCH 4/4] test: example of adding spy call inline Signed-off-by: Mike Fiedler --- tests/unit/admin/views/test_users.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit/admin/views/test_users.py b/tests/unit/admin/views/test_users.py index 1853aa4e1c81..57cb7f2ddf37 100644 --- a/tests/unit/admin/views/test_users.py +++ b/tests/unit/admin/views/test_users.py @@ -20,6 +20,7 @@ from sqlalchemy.orm import joinedload from webob.multidict import MultiDict, NoVars +from warehouse.accounts import IDomainStatusService from warehouse.accounts.interfaces import IEmailBreachedService, IUserService from warehouse.accounts.models import ( DisableReason, @@ -1542,7 +1543,7 @@ def test_no_recovery_codes_provided(self, db_request, monkeypatch, user_service) class TestUserEmailDomainCheck: - def test_user_email_domain_check(self, db_request, domain_status_service): + def test_user_email_domain_check(self, db_request, domain_status_service, mocker): user = UserFactory.create(with_verified_primary_email=True) db_request.POST["email_address"] = user.primary_email.email db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/foobar") @@ -1550,6 +1551,8 @@ def test_user_email_domain_check(self, db_request, domain_status_service): flash=pretend.call_recorder(lambda *a, **kw: None) ) + spied_req = mocker.spy(db_request, "find_service") + result = views.user_email_domain_check(user, db_request) assert isinstance(result, HTTPSeeOther) @@ -1571,3 +1574,5 @@ def test_user_email_domain_check(self, db_request, domain_status_service): assert domain_status_service.get_domain_status.spy_return_list == [["active"]] # most recent exception assert domain_status_service.get_domain_status.spy_exception is None + + spied_req.assert_called_once_with(IDomainStatusService)