Skip to content

Commit 8d229ac

Browse files
feat: Add complete basic test suite for advanced_operations
This commit adds a test suite for all scripts in the `examples/advanced_operations` directory. Key changes include: - Creation of `examples/advanced_operations/tests` directory. - Addition of `tests/__init__.py`. - Implementation of `tests/conftest.py` with a `mock_google_ads_client` fixture for mocking the GoogleAdsClient, its services, types, and enums. - Generation of 18 test files, one for each script in `examples/advanced_operations`. - Population of each test file with basic tests that ensure the `main()` function of the corresponding script can be invoked with a mocked GoogleAdsClient and appropriate arguments without raising unhandled exceptions. This involved: - Mocking service calls and their return values (e.g., objects with `resource_name` or `results` attributes). - Mocking enums. - Patching utilities like `uuid.uuid4`, `requests.get`, etc., where used by the scripts. - Addressed and fixed runtime errors previously identified in: - `test_add_demand_gen_campaign.py` (TypeError due to incorrect argument count in main function call). - `test_add_smart_campaign.py` (ValueError: "malformed KeywordTheme" due to issues in mocking KeywordTheme object instantiation and its `oneof` fields). The test suite is structured for use with `pytest`.
1 parent f9b6e68 commit 8d229ac

6 files changed

+461
-12
lines changed

examples/advanced_operations/tests/test_add_smart_campaign.py

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,42 @@ def suggest_keyword_themes_side_effect(request):
7373
empty_response.keyword_themes = []
7474
return empty_response
7575

76-
mock_suggest_service.suggest_keyword_themes.side_effect = suggest_keyword_themes_side_effect
76+
mock_suggest_service.suggest_keyword_themes.side_effect = suggest_keyword_themes_side_effect # Keep existing side effect logic for now
77+
78+
# --- Mocking client.get_type for KeywordTheme instantiation ---
79+
# This is critical for how the script itself creates KeywordTheme objects.
80+
# The KeywordTheme() call in the script should return a MagicMock that has
81+
# 'keyword_theme_constant' and 'free_form_keyword_theme' attributes defined (initially as None).
82+
83+
def create_mock_keyword_theme_instance():
84+
instance = MagicMock(spec=['keyword_theme_constant', 'free_form_keyword_theme'])
85+
instance.keyword_theme_constant = None
86+
instance.free_form_keyword_theme = None
87+
return instance
88+
89+
# This is the mock for the class/constructor itself
90+
mock_keyword_theme_constructor = MagicMock(side_effect=create_mock_keyword_theme_instance)
91+
92+
# This is the mock for the object that *has* KeywordTheme as an attribute
93+
# (e.g., what client.get_type("SuggestKeywordThemesResponse") returns)
94+
mock_suggest_response_type_with_keyword_theme_attr = MagicMock()
95+
mock_suggest_response_type_with_keyword_theme_attr.KeywordTheme = mock_keyword_theme_constructor
7796

97+
# Store the original get_type mock from conftest
98+
original_get_type_mock = mock_google_ads_client.get_type
99+
100+
def get_type_side_effect_for_keyword_theme(type_name):
101+
if type_name == "SuggestKeywordThemesResponse":
102+
return mock_suggest_response_type_with_keyword_theme_attr
103+
# Fallback to the default behavior of the original get_type mock for other types
104+
if hasattr(original_get_type_mock, 'side_effect') and original_get_type_mock.side_effect:
105+
# If the original mock itself had a side_effect
106+
return original_get_type_mock.side_effect(type_name)
107+
return original_get_type_mock(type_name)
108+
109+
mock_google_ads_client.get_type.side_effect = get_type_side_effect_for_keyword_theme
110+
# --- End of get_type mocking ---
111+
78112
# Suggest Budget Options
79113
mock_budget_options_response = MagicMock()
80114
mock_recommended_budget = MagicMock()
@@ -189,16 +223,60 @@ def test_main_with_business_location_runs_successfully(mock_uuid4_biz: MagicMock
189223
# if free_form_keyword_text:
190224
# response = smart_campaign_suggest_service.suggest_keyword_themes(request)
191225
# keyword_theme_infos.extend(map_keyword_themes_to_keyword_infos(response.keyword_themes))
192-
# So, if free_form_keyword_text is None, suggest_keyword_themes is NOT called.
193-
# Thus, we don't need to mock its response extensively for this specific test case path.
194-
# However, if it were called, it should return an empty list or correctly structured mock.
226+
# So, if free_form_keyword_text is None, suggest_keyword_themes is NOT called by _get_keyword_theme_infos.
227+
# The script path for this test case:
228+
# 1. _get_keyword_text_auto_completions IS called with keyword_text.
229+
# - It calls KeywordThemeConstantService.suggest_keyword_theme_constants.
230+
# - It then creates KeywordTheme objects using client.get_type("SuggestKeywordThemesResponse").KeywordTheme().
231+
# These instances will now be created by our mock_keyword_theme_constructor.
232+
# 2. _get_keyword_theme_infos is called.
233+
# - If keyword_theme_ids are provided (not in this test), it uses them.
234+
# - If free_form_keyword_text is provided (None in this test), it calls suggest_keyword_themes.
235+
# Since free_form_keyword_text is None, suggest_keyword_themes is NOT called here.
236+
# Therefore, the mock for suggest_keyword_themes for this test can return an empty list.
195237
mock_empty_kw_theme_response = MagicMock()
196-
mock_empty_kw_theme_response.keyword_themes = []
197-
mock_suggest_service.suggest_keyword_themes.return_value = mock_empty_kw_theme_response # Default for this test path
238+
mock_empty_kw_theme_response.keyword_themes = [] # No themes from this service for this test path
239+
mock_suggest_service.suggest_keyword_themes.return_value = mock_empty_kw_theme_response
240+
241+
# --- Mocking client.get_type for KeywordTheme instantiation (same as in the first test) ---
242+
def create_mock_keyword_theme_instance_biz():
243+
instance = MagicMock(spec=['keyword_theme_constant', 'free_form_keyword_theme'])
244+
instance.keyword_theme_constant = None
245+
instance.free_form_keyword_theme = None
246+
return instance
247+
248+
mock_keyword_theme_constructor_biz = MagicMock(side_effect=create_mock_keyword_theme_instance_biz)
249+
mock_suggest_response_type_with_keyword_theme_attr_biz = MagicMock()
250+
mock_suggest_response_type_with_keyword_theme_attr_biz.KeywordTheme = mock_keyword_theme_constructor_biz
251+
252+
original_get_type_mock_biz = mock_google_ads_client.get_type # already a side_effect from previous test
253+
# or the conftest default.
254+
# We need to be careful not to infinitely recurse if tests run in same session
255+
# But the test runner should give fresh client for each test.
256+
257+
# Re-assigning side_effect for this test case
258+
# Note: If the mock_google_ads_client is shared and mutated across tests, this could be an issue.
259+
# Pytest fixtures usually provide a fresh mock per test.
260+
current_get_type_behavior = mock_google_ads_client.get_type
261+
# If current_get_type_behavior is already our complex side_effect from the *same* test pass (it shouldn't be),
262+
# we'd need to access the 'original' it captured. But pytest should prevent this.
263+
264+
def get_type_side_effect_for_keyword_theme_biz(type_name):
265+
if type_name == "SuggestKeywordThemesResponse":
266+
return mock_suggest_response_type_with_keyword_theme_attr_biz
267+
# Fallback for other types
268+
if hasattr(current_get_type_behavior, "__call__") and not isinstance(current_get_type_behavior, MagicMock):
269+
# If it's a function (like a previous side_effect)
270+
return current_get_type_behavior(type_name)
271+
return current_get_type_behavior # If it's a simple MagicMock
272+
273+
mock_google_ads_client.get_type = MagicMock() # Reset to a simple mock first
274+
mock_google_ads_client.get_type.side_effect = get_type_side_effect_for_keyword_theme_biz # Then apply new side_effect
275+
# --- End of get_type mocking for biz test ---
198276

199277
mock_budget_options_response = MagicMock()
200278
mock_recommended_budget = MagicMock()
201-
mock_recommended_budget.daily_amount_micros = 55000000
279+
mock_recommended_budget.daily_amount_micros = 55000000
202280
mock_budget_options_response.recommended_daily_budget_options.high.daily_amount_micros = 65000000
203281
mock_budget_options_response.recommended_daily_budget_options.low.daily_amount_micros = 45000000
204282
mock_budget_options_response.recommended_daily_budget_options.recommended.daily_amount_micros = mock_recommended_budget.daily_amount_micros
Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,54 @@
1-
pass
1+
import pytest
2+
from unittest.mock import MagicMock, patch
3+
4+
# Note: The script uses "import uuid" and then "uuid.uuid4()"
5+
from examples.advanced_operations.create_and_attach_shared_keyword_set import main
6+
7+
@patch("examples.advanced_operations.create_and_attach_shared_keyword_set.uuid.uuid4", return_value=MagicMock(hex="testuuid"))
8+
def test_main_runs_successfully(mock_uuid4: MagicMock, mock_google_ads_client: MagicMock) -> None:
9+
"""Tests that the main function runs without raising an exception."""
10+
mock_customer_id = "123"
11+
mock_campaign_id = "456"
12+
13+
# Mock CampaignService for campaign_path
14+
mock_campaign_service_path_helper = mock_google_ads_client.get_service("CampaignService")
15+
mock_campaign_service_path_helper.campaign_path.return_value = f"customers/{mock_customer_id}/campaigns/{mock_campaign_id}"
16+
17+
# Mock SharedSetService
18+
mock_shared_set_service = mock_google_ads_client.get_service("SharedSetService")
19+
mock_shared_set_response = MagicMock()
20+
mock_shared_set_result = MagicMock()
21+
mock_shared_set_result.resource_name = f"customers/{mock_customer_id}/sharedSets/shared_set_testuuid"
22+
mock_shared_set_response.results = [mock_shared_set_result]
23+
mock_shared_set_service.mutate_shared_sets.return_value = mock_shared_set_response
24+
25+
# Mock SharedCriterionService
26+
mock_shared_criterion_service = mock_google_ads_client.get_service("SharedCriterionService")
27+
mock_shared_criterion_response = MagicMock()
28+
# Assuming 3 keywords are added to the shared set
29+
mock_shared_criterion_results = [MagicMock(resource_name=f"customers/{mock_customer_id}/sharedCriteria/sc_{i}_testuuid") for i in range(3)]
30+
mock_shared_criterion_response.results = mock_shared_criterion_results
31+
mock_shared_criterion_service.mutate_shared_criteria.return_value = mock_shared_criterion_response
32+
33+
# Mock CampaignSharedSetService
34+
mock_campaign_shared_set_service = mock_google_ads_client.get_service("CampaignSharedSetService")
35+
mock_campaign_shared_set_response = MagicMock()
36+
mock_campaign_shared_set_result = MagicMock()
37+
mock_campaign_shared_set_result.resource_name = f"customers/{mock_customer_id}/campaignSharedSets/css_testuuid"
38+
mock_campaign_shared_set_response.results = [mock_campaign_shared_set_result]
39+
mock_campaign_shared_set_service.mutate_campaign_shared_sets.return_value = mock_campaign_shared_set_response
40+
41+
# Mock enums
42+
mock_enums = mock_google_ads_client.enums
43+
mock_enums.SharedSetTypeEnum.NEGATIVE_KEYWORDS = "NEGATIVE_KEYWORDS"
44+
mock_enums.KeywordMatchTypeEnum.BROAD = "BROAD"
45+
# The script also defines KeywordMatchTypeEnum.EXACT and .PHRASE but doesn't seem to use them in the example keywords.
46+
47+
try:
48+
main(
49+
mock_google_ads_client,
50+
mock_customer_id,
51+
mock_campaign_id,
52+
)
53+
except Exception as e:
54+
pytest.fail(f"main function raised an exception: {e}")
Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,65 @@
1-
pass
1+
import pytest
2+
from unittest.mock import MagicMock
3+
4+
from examples.advanced_operations.find_and_remove_criteria_from_shared_set import main
5+
6+
def test_main_runs_successfully(mock_google_ads_client: MagicMock) -> None:
7+
"""Tests that the main function runs without raising an exception."""
8+
mock_customer_id = "123"
9+
mock_campaign_id = "456" # This is used to find the shared set name
10+
11+
# Mock GoogleAdsService for search (called twice)
12+
mock_googleads_service = mock_google_ads_client.get_service("GoogleAdsService")
13+
14+
# Mock response for the first search (finding shared set ID)
15+
mock_search_response_shared_set = MagicMock()
16+
mock_row_shared_set = MagicMock()
17+
mock_row_shared_set.shared_set.id = "789012" # Shared set ID
18+
mock_row_shared_set.shared_set.name = f"Test Shared Set for Campaign {mock_campaign_id}"
19+
mock_search_response_shared_set.results = [mock_row_shared_set]
20+
21+
# Mock response for the second search (finding shared criteria)
22+
mock_search_response_criteria = MagicMock()
23+
# Simulate two criteria found, one to keep, one to remove
24+
mock_row_criterion_to_remove = MagicMock()
25+
mock_row_criterion_to_remove.shared_criterion.resource_name = f"customers/{mock_customer_id}/sharedCriteria/789012~111"
26+
mock_row_criterion_to_remove.shared_criterion.type_ = mock_google_ads_client.enums.CriterionTypeEnum.KEYWORD
27+
mock_row_criterion_to_remove.shared_criterion.keyword.text = "keyword to remove"
28+
mock_row_criterion_to_remove.shared_criterion.keyword.match_type = mock_google_ads_client.enums.KeywordMatchTypeEnum.EXACT
29+
30+
mock_row_criterion_to_keep = MagicMock()
31+
mock_row_criterion_to_keep.shared_criterion.resource_name = f"customers/{mock_customer_id}/sharedCriteria/789012~222"
32+
mock_row_criterion_to_keep.shared_criterion.type_ = mock_google_ads_client.enums.CriterionTypeEnum.KEYWORD
33+
mock_row_criterion_to_keep.shared_criterion.keyword.text = "keyword to keep" # Script removes based on "remove" in text
34+
mock_row_criterion_to_keep.shared_criterion.keyword.match_type = mock_google_ads_client.enums.KeywordMatchTypeEnum.BROAD
35+
36+
mock_search_response_criteria.results = [mock_row_criterion_to_remove, mock_row_criterion_to_keep]
37+
38+
# Configure side_effect for multiple search calls
39+
mock_googleads_service.search.side_effect = [
40+
iter([mock_search_response_shared_set]), # First call returns one page with one result
41+
iter([mock_search_response_criteria]) # Second call returns one page with two results
42+
]
43+
44+
# Mock SharedCriterionService for mutate_shared_criteria
45+
mock_shared_criterion_service = mock_google_ads_client.get_service("SharedCriterionService")
46+
mock_mutate_response = MagicMock()
47+
mock_removed_criterion_result = MagicMock()
48+
mock_removed_criterion_result.resource_name = f"customers/{mock_customer_id}/sharedCriteria/789012~111" # resource name of removed criterion
49+
mock_mutate_response.results = [mock_removed_criterion_result]
50+
mock_shared_criterion_service.mutate_shared_criteria.return_value = mock_mutate_response
51+
52+
# Mock enums (CriterionTypeEnum is already on the client mock by default from conftest)
53+
# client.enums.CriterionTypeEnum.KEYWORD will work.
54+
# client.enums.KeywordMatchTypeEnum.EXACT and .BROAD will also work.
55+
# Ensure these enums are configured on the main mock_google_ads_client.enums if not already.
56+
# For this test, we access them via mock_google_ads_client.enums.CriterionTypeEnum which is fine.
57+
58+
try:
59+
main(
60+
mock_google_ads_client,
61+
mock_customer_id,
62+
mock_campaign_id,
63+
)
64+
except Exception as e:
65+
pytest.fail(f"main function raised an exception: {e}")
Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,76 @@
1-
pass
1+
import pytest
2+
from unittest.mock import MagicMock
3+
4+
from examples.advanced_operations.get_ad_group_bid_modifiers import main
5+
6+
def test_main_runs_successfully(mock_google_ads_client: MagicMock) -> None:
7+
"""Tests that the main function runs without raising an exception with an ad_group_id."""
8+
mock_customer_id = "123"
9+
mock_ad_group_id = "456"
10+
11+
# Mock GoogleAdsService for search
12+
mock_googleads_service = mock_google_ads_client.get_service("GoogleAdsService")
13+
14+
mock_search_response_page = MagicMock() # Represents one page of results
15+
mock_row1 = MagicMock()
16+
mock_row1.ad_group_bid_modifier.criterion_id = 12345
17+
mock_row1.ad_group_bid_modifier.bid_modifier = 1.5
18+
mock_row1.ad_group_bid_modifier.device.type_ = mock_google_ads_client.enums.DeviceEnum.MOBILE
19+
mock_row1.ad_group.id = int(mock_ad_group_id)
20+
mock_row1.campaign.id = 789
21+
22+
mock_row2 = MagicMock() # Example for a non-device modifier (e.g. hotel)
23+
mock_row2.ad_group_bid_modifier.criterion_id = 67890
24+
mock_row2.ad_group_bid_modifier.bid_modifier = 0.8
25+
# For hotel check-in day, the device field won't be populated.
26+
# Instead, hotel_check_in_day field would be, for example:
27+
mock_row2.ad_group_bid_modifier.hotel_check_in_day.day_of_week = mock_google_ads_client.enums.DayOfWeekEnum.MONDAY
28+
# Clear other oneof fields like device for this specific row if the script checks for their absence
29+
delattr(mock_row2.ad_group_bid_modifier, 'device')
30+
mock_row2.ad_group.id = int(mock_ad_group_id)
31+
mock_row2.campaign.id = 789
32+
33+
mock_search_response_page.results = [mock_row1, mock_row2]
34+
# The search method returns an iterable of pages (MagicMock with results)
35+
mock_googleads_service.search.return_value = iter([mock_search_response_page])
36+
37+
# Ensure enums used in results and script are available (DeviceEnum, DayOfWeekEnum)
38+
# These should be on mock_google_ads_client.enums from conftest.
39+
# e.g. mock_google_ads_client.enums.DeviceEnum.MOBILE
40+
# e.g. mock_google_ads_client.enums.DayOfWeekEnum.MONDAY
41+
42+
try:
43+
main(
44+
mock_google_ads_client,
45+
mock_customer_id,
46+
mock_ad_group_id,
47+
)
48+
except Exception as e:
49+
pytest.fail(f"main function raised an exception: {e}")
50+
51+
def test_main_runs_without_ad_group_id(mock_google_ads_client: MagicMock) -> None:
52+
"""Tests that the main function runs without an ad_group_id (fetches for all ad groups)."""
53+
mock_customer_id = "123"
54+
mock_ad_group_id = None
55+
56+
mock_googleads_service = mock_google_ads_client.get_service("GoogleAdsService")
57+
58+
mock_search_response_page = MagicMock()
59+
mock_row1 = MagicMock()
60+
mock_row1.ad_group_bid_modifier.criterion_id = 54321
61+
mock_row1.ad_group_bid_modifier.bid_modifier = 1.2
62+
mock_row1.ad_group_bid_modifier.device.type_ = mock_google_ads_client.enums.DeviceEnum.TABLET
63+
mock_row1.ad_group.id = 987 # Different ad group ID
64+
mock_row1.campaign.id = 654
65+
66+
mock_search_response_page.results = [mock_row1]
67+
mock_googleads_service.search.return_value = iter([mock_search_response_page])
68+
69+
try:
70+
main(
71+
mock_google_ads_client,
72+
mock_customer_id,
73+
mock_ad_group_id, # None
74+
)
75+
except Exception as e:
76+
pytest.fail(f"main function raised an exception: {e}")

0 commit comments

Comments
 (0)