Skip to content

Commit 1ea88ac

Browse files
fix: Resolve runtime errors in smart campaign and bid modifier tests
This commit addresses several runtime errors identified in the generated test suite: 1. **test_add_smart_campaign.py:** * Resolved `RecursionError` by ensuring the `get_type` side_effect fallback calls the original mock's `__call__` method or returns a fresh `MagicMock`, preventing infinite recursion. This involved resetting `client.get_type` to a basic mock at the start of each test before applying a new side_effect. * Fixed `TypeError: argument of type 'MagicMock' is not iterable` by ensuring that the `SmartCampaignSuggestionInfo` mock (returned by the `get_type` side_effect) has `location_list.locations` and `ad_schedules` attributes initialized as actual Python lists (`[]`), allowing the script's `.append()` calls to work correctly. * Refined the mocking of `KeywordTheme` instantiation via `client.get_type("SuggestKeywordThemesResponse").KeywordTheme` to ensure created mocks are compatible with the script's `in` operator checks for `oneof` fields. 2. **test_get_ad_group_bid_modifiers.py:** * Resolved `AttributeError: type object 'MagicMock' has no attribute 'pb'`. This was caused by the script using `type(modifier).pb(modifier)`. * Introduced a custom class `MockAdGroupBidModifierModel` within the test file. This class has a `pb` class method that returns a `MagicMock` configured with a `WhichOneof` method, mimicking the behavior of actual Google Ads library model objects. * Instances of `MockAdGroupBidModifierModel` are now used for `row.ad_group_bid_modifier` in the mocked search results. * Ensured that enums used by the script (e.g., `DeviceEnum.MOBILE`) are mocked to have a `.name` attribute for print statements.
1 parent 8d229ac commit 1ea88ac

File tree

2 files changed

+181
-91
lines changed

2 files changed

+181
-91
lines changed

examples/advanced_operations/tests/test_add_smart_campaign.py

Lines changed: 75 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -75,38 +75,53 @@ def suggest_keyword_themes_side_effect(request):
7575

7676
mock_suggest_service.suggest_keyword_themes.side_effect = suggest_keyword_themes_side_effect # Keep existing side effect logic for now
7777

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
96-
97-
# Store the original get_type mock from conftest
98-
original_get_type_mock = mock_google_ads_client.get_type
78+
# --- Mocking client.get_type for various types used in the script ---
79+
mock_google_ads_client.get_type = MagicMock() # Reset to a simple mock first
9980

100-
def get_type_side_effect_for_keyword_theme(type_name):
81+
# Mock for KeywordTheme objects produced by KeywordTheme()
82+
# This instance will be returned when KeywordTheme() is called in the script.
83+
mock_keyword_theme_instance = MagicMock(spec=['keyword_theme_constant', 'free_form_keyword_theme'])
84+
mock_keyword_theme_instance.keyword_theme_constant = None
85+
mock_keyword_theme_instance.free_form_keyword_theme = None
86+
# This is the mock for the KeywordTheme "class" or "constructor"
87+
mock_keyword_theme_constructor = MagicMock(return_value=mock_keyword_theme_instance)
88+
89+
# This is the mock for the type that *contains* KeywordTheme
90+
# (i.e., what client.get_type("SuggestKeywordThemesResponse") returns)
91+
mock_suggest_keyword_themes_response_type = MagicMock()
92+
mock_suggest_keyword_themes_response_type.KeywordTheme = mock_keyword_theme_constructor
93+
94+
# Mock for SmartCampaignSuggestionInfo
95+
mock_smart_campaign_suggestion_info_instance = MagicMock()
96+
mock_smart_campaign_suggestion_info_instance.location_list = MagicMock()
97+
mock_smart_campaign_suggestion_info_instance.location_list.locations = [] # Must be a list for .append()
98+
mock_smart_campaign_suggestion_info_instance.ad_schedules = [] # Must be a list for .append()
99+
100+
# Mock for LocationInfo and AdScheduleInfo (simple mocks are fine)
101+
mock_location_info_instance = MagicMock()
102+
mock_ad_schedule_info_instance = MagicMock()
103+
104+
def new_get_type_side_effect(type_name):
101105
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
106+
return mock_suggest_keyword_themes_response_type
107+
elif type_name == "SmartCampaignSuggestionInfo":
108+
# Important: return the instance directly if the script does client.get_type("SmartCampaignSuggestionInfo")
109+
# and expects to set attributes on it. If it calls it like a constructor, then this should return a mock constructor.
110+
# The script does: suggestion_info = client.get_type("SmartCampaignSuggestionInfo")
111+
# then suggestion_info.location_list = ...
112+
# So, returning the instance directly is correct.
113+
return mock_smart_campaign_suggestion_info_instance
114+
elif type_name == "LocationInfo":
115+
# Script does: location = client.get_type("LocationInfo")
116+
return mock_location_info_instance
117+
elif type_name == "AdScheduleInfo":
118+
# Script does: ad_schedule = client.get_type("AdScheduleInfo")
119+
return mock_ad_schedule_info_instance
120+
else:
121+
# Fallback for any other type
122+
return MagicMock()
123+
124+
mock_google_ads_client.get_type.side_effect = new_get_type_side_effect
110125
# --- End of get_type mocking ---
111126

112127
# Suggest Budget Options
@@ -238,40 +253,38 @@ def test_main_with_business_location_runs_successfully(mock_uuid4_biz: MagicMock
238253
mock_empty_kw_theme_response.keyword_themes = [] # No themes from this service for this test path
239254
mock_suggest_service.suggest_keyword_themes.return_value = mock_empty_kw_theme_response
240255

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
256+
# --- Mocking client.get_type for various types (similar to the first test) ---
257+
mock_google_ads_client.get_type = MagicMock() # Reset to a simple mock
258+
259+
mock_keyword_theme_instance_biz = MagicMock(spec=['keyword_theme_constant', 'free_form_keyword_theme'])
260+
mock_keyword_theme_instance_biz.keyword_theme_constant = None
261+
mock_keyword_theme_instance_biz.free_form_keyword_theme = None
262+
mock_keyword_theme_constructor_biz = MagicMock(return_value=mock_keyword_theme_instance_biz)
263+
264+
mock_suggest_keyword_themes_response_type_biz = MagicMock()
265+
mock_suggest_keyword_themes_response_type_biz.KeywordTheme = mock_keyword_theme_constructor_biz
266+
267+
mock_smart_campaign_suggestion_info_instance_biz = MagicMock()
268+
mock_smart_campaign_suggestion_info_instance_biz.location_list = MagicMock()
269+
mock_smart_campaign_suggestion_info_instance_biz.location_list.locations = []
270+
mock_smart_campaign_suggestion_info_instance_biz.ad_schedules = []
251271

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):
272+
mock_location_info_instance_biz = MagicMock()
273+
mock_ad_schedule_info_instance_biz = MagicMock()
274+
275+
def new_get_type_side_effect_biz(type_name):
265276
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
277+
return mock_suggest_keyword_themes_response_type_biz
278+
elif type_name == "SmartCampaignSuggestionInfo":
279+
return mock_smart_campaign_suggestion_info_instance_biz
280+
elif type_name == "LocationInfo":
281+
return mock_location_info_instance_biz
282+
elif type_name == "AdScheduleInfo":
283+
return mock_ad_schedule_info_instance_biz
284+
else:
285+
return MagicMock()
286+
287+
mock_google_ads_client.get_type.side_effect = new_get_type_side_effect_biz
275288
# --- End of get_type mocking for biz test ---
276289

277290
mock_budget_options_response = MagicMock()

examples/advanced_operations/tests/test_get_ad_group_bid_modifiers.py

Lines changed: 106 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,109 @@
33

44
from examples.advanced_operations.get_ad_group_bid_modifiers import main
55

6+
# --- Helper Mock Class ---
7+
class MockAdGroupBidModifierModel:
8+
def __init__(self, client_enums):
9+
# Attributes accessed directly by the script's print statement
10+
self.criterion_id = MagicMock(name="criterion_id")
11+
self.bid_modifier = MagicMock(name="bid_modifier")
12+
13+
self.device = MagicMock(name="device_criterion")
14+
# Ensure type_ itself can have .name called on it
15+
self.device.type_ = MagicMock(name="device_type_enum_value")
16+
self.device.type_.name = "UNKNOWN_DEVICE" # Default .name for the enum value
17+
18+
# Hotel criteria attributes
19+
self.hotel_date_selection_type = MagicMock(name="hotel_date_selection_type_criterion")
20+
self.hotel_date_selection_type.type_ = MagicMock(name="hotel_date_selection_type_enum_value")
21+
self.hotel_date_selection_type.type_.name = "UNKNOWN_HOTEL_DATE_SELECTION"
22+
23+
self.hotel_advance_booking_window = MagicMock(name="hotel_advance_booking_window_criterion")
24+
self.hotel_advance_booking_window.min_days = None
25+
self.hotel_advance_booking_window.max_days = None
26+
27+
self.hotel_length_of_stay = MagicMock(name="hotel_length_of_stay_criterion")
28+
self.hotel_length_of_stay.min_nights = None
29+
self.hotel_length_of_stay.max_nights = None
30+
31+
self.hotel_check_in_day = MagicMock(name="hotel_check_in_day_criterion")
32+
self.hotel_check_in_day.day_of_week = MagicMock(name="day_of_week_enum_value")
33+
self.hotel_check_in_day.day_of_week.name = "UNSPECIFIED_DAY"
34+
35+
self.hotel_check_in_date_range = MagicMock(name="hotel_check_in_date_range_criterion")
36+
self.hotel_check_in_date_range.start_date = None
37+
self.hotel_check_in_date_range.end_date = None
38+
39+
# This will store the name of the 'oneof' criterion field (e.g., "device")
40+
self._active_criterion_field = None # Default to None, must be set by test
41+
42+
@classmethod
43+
def pb(cls, instance_self):
44+
mock_pb_message = MagicMock(name="pb_message")
45+
mock_pb_message.WhichOneof.return_value = instance_self._active_criterion_field
46+
return mock_pb_message
47+
48+
def set_active_criterion_field(self, field_name):
49+
self._active_criterion_field = field_name
50+
# Basic way to somewhat mimic oneof: clear other fields if one is set.
51+
# More robust would be to ensure only the active field has a non-default/non-None value.
52+
# For this mock, primarily WhichOneof matters for the script's logic.
53+
all_criteria_fields = [
54+
"device", "hotel_date_selection_type", "hotel_advance_booking_window",
55+
"hotel_length_of_stay", "hotel_check_in_day", "hotel_check_in_date_range"
56+
]
57+
if field_name not in all_criteria_fields and field_name is not None:
58+
raise ValueError(f"Unknown criterion field: {field_name}")
59+
return self
60+
61+
# --- Test Functions ---
662
def test_main_runs_successfully(mock_google_ads_client: MagicMock) -> None:
763
"""Tests that the main function runs without raising an exception with an ad_group_id."""
864
mock_customer_id = "123"
965
mock_ad_group_id = "456"
1066

11-
# Mock GoogleAdsService for search
67+
# --- Mock Enums (ensure they have .name attribute) ---
68+
mock_google_ads_client.enums.DeviceEnum.MOBILE.name = "MOBILE_DEVICE"
69+
mock_google_ads_client.enums.DeviceEnum.TABLET.name = "TABLET_DEVICE" # Just in case
70+
mock_google_ads_client.enums.DeviceEnum.DESKTOP.name = "DESKTOP_DEVICE" # Just in case
71+
mock_google_ads_client.enums.DeviceEnum.UNKNOWN.name = "UNKNOWN_DEVICE" # Default
72+
73+
mock_google_ads_client.enums.DayOfWeekEnum.MONDAY.name = "MONDAY"
74+
mock_google_ads_client.enums.DayOfWeekEnum.UNSPECIFIED.name = "UNSPECIFIED_DOW" # Default
75+
76+
# Add other hotel enums if specific paths are tested, e.g., HotelDateSelectionTypeEnum
77+
mock_google_ads_client.enums.HotelDateSelectionTypeEnum.DEFAULT_SELECTION.name = "DEFAULT_SELECTION"
78+
mock_google_ads_client.enums.HotelDateSelectionTypeEnum.UNKNOWN.name = "UNKNOWN_HDST" # Default
79+
80+
# --- Mock GoogleAdsService for search ---
1281
mock_googleads_service = mock_google_ads_client.get_service("GoogleAdsService")
1382

14-
mock_search_response_page = MagicMock() # Represents one page of results
83+
# Row 1: Device Modifier
1584
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
85+
modifier1 = MockAdGroupBidModifierModel(mock_google_ads_client.enums)
86+
modifier1.criterion_id = "1001"
87+
modifier1.bid_modifier = 1.5
88+
modifier1.set_active_criterion_field("device")
89+
modifier1.device.type_ = mock_google_ads_client.enums.DeviceEnum.MOBILE
90+
mock_row1.ad_group_bid_modifier = modifier1
1991
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')
92+
mock_row1.campaign.id = "789"
93+
94+
# Row 2: Hotel Check-in Day Modifier
95+
mock_row2 = MagicMock()
96+
modifier2 = MockAdGroupBidModifierModel(mock_google_ads_client.enums)
97+
modifier2.criterion_id = "2002"
98+
modifier2.bid_modifier = 0.8
99+
modifier2.set_active_criterion_field("hotel_check_in_day")
100+
modifier2.hotel_check_in_day.day_of_week = mock_google_ads_client.enums.DayOfWeekEnum.MONDAY
101+
mock_row2.ad_group_bid_modifier = modifier2
30102
mock_row2.ad_group.id = int(mock_ad_group_id)
31-
mock_row2.campaign.id = 789
103+
mock_row2.campaign.id = "790"
32104

105+
mock_search_response_page = MagicMock()
33106
mock_search_response_page.results = [mock_row1, mock_row2]
34-
# The search method returns an iterable of pages (MagicMock with results)
35107
mock_googleads_service.search.return_value = iter([mock_search_response_page])
36108

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-
42109
try:
43110
main(
44111
mock_google_ads_client,
@@ -51,18 +118,28 @@ def test_main_runs_successfully(mock_google_ads_client: MagicMock) -> None:
51118
def test_main_runs_without_ad_group_id(mock_google_ads_client: MagicMock) -> None:
52119
"""Tests that the main function runs without an ad_group_id (fetches for all ad groups)."""
53120
mock_customer_id = "123"
54-
mock_ad_group_id = None
121+
mock_ad_group_id = None # Key difference for this test
122+
123+
# --- Mock Enums (ensure they have .name attribute) ---
124+
# Ensure these are available on the mock_google_ads_client.enums object
125+
# The conftest should provide the base enums, we just need to ensure .name is on values.
126+
mock_google_ads_client.enums.DeviceEnum.TABLET.name = "TABLET_DEVICE"
127+
mock_google_ads_client.enums.DeviceEnum.UNKNOWN.name = "UNKNOWN_DEVICE"
55128

129+
# --- Mock GoogleAdsService for search ---
56130
mock_googleads_service = mock_google_ads_client.get_service("GoogleAdsService")
57131

58-
mock_search_response_page = MagicMock()
59132
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
133+
modifier1 = MockAdGroupBidModifierModel(mock_google_ads_client.enums)
134+
modifier1.criterion_id = "3003"
135+
modifier1.bid_modifier = 1.2
136+
modifier1.set_active_criterion_field("device")
137+
modifier1.device.type_ = mock_google_ads_client.enums.DeviceEnum.TABLET
138+
mock_row1.ad_group_bid_modifier = modifier1
139+
mock_row1.ad_group.id = "987" # Different ad group ID
140+
mock_row1.campaign.id = "654"
65141

142+
mock_search_response_page = MagicMock()
66143
mock_search_response_page.results = [mock_row1]
67144
mock_googleads_service.search.return_value = iter([mock_search_response_page])
68145

0 commit comments

Comments
 (0)