Skip to content

Commit cf5652f

Browse files
feat(examples): Update targeting examples to v19 and add test suite
- Ensured all Python scripts in examples/targeting/ use Google Ads API v19. - Created a new test suite in examples/targeting/tests/. - Added __init__.py to the new test directory. - Implemented basic unit tests for: - add_campaign_targeting_criteria.py - add_customer_negative_criteria.py - add_demographic_targeting_criteria.py - get_geo_target_constants_by_names.py The tests mock the GoogleAdsClient and relevant services to verify that API calls are made with the expected parameters and that criteria are constructed correctly.
1 parent c547b4e commit cf5652f

6 files changed

Lines changed: 373 additions & 0 deletions

examples/targeting/tests/.keep

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This file is intentionally left blank.
2+
# It is used to ensure the directory is tracked by Git.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This file is intentionally left blank.
2+
# It marks this directory as a Python package.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import unittest
2+
from unittest.mock import MagicMock, patch, call
3+
import sys
4+
5+
# Add the examples directory to the system path to import the script
6+
sys.path.append(".")
7+
8+
from examples.targeting.add_campaign_targeting_criteria import main
9+
10+
class TestAddCampaignTargetingCriteria(unittest.TestCase):
11+
@patch("examples.targeting.add_campaign_targeting_criteria.GoogleAdsClient.load_from_storage")
12+
def test_main_function_calls(self, mock_load_client):
13+
# Mock customer and campaign IDs
14+
MOCK_CUSTOMER_ID = "1234567890"
15+
MOCK_CAMPAIGN_ID = "9876543210"
16+
MOCK_KEYWORD_TEXT = "negative keyword"
17+
MOCK_LOCATION_ID = "21167" # New York
18+
19+
# Create a mock GoogleAdsClient
20+
mock_google_ads_client = MagicMock()
21+
mock_load_client.return_value = mock_google_ads_client
22+
23+
# Mock the CampaignCriterionService
24+
mock_campaign_criterion_service = MagicMock()
25+
mock_google_ads_client.get_service.return_value = mock_campaign_criterion_service
26+
27+
# Mock the get_type method for various types
28+
mock_campaign_criterion_operation = MagicMock()
29+
mock_keyword_info = MagicMock()
30+
mock_location_info = MagicMock()
31+
mock_proximity_info = MagicMock()
32+
mock_address_info = MagicMock()
33+
34+
# Configure client.get_type to return appropriate mocks
35+
def get_type_side_effect(type_name, version=None):
36+
if type_name == "CampaignCriterionOperation":
37+
# Return a new MagicMock each time to ensure operations are distinct
38+
return MagicMock()
39+
elif type_name == "KeywordInfo":
40+
return mock_keyword_info
41+
elif type_name == "LocationInfo":
42+
return mock_location_info
43+
elif type_name == "ProximityInfo":
44+
return mock_proximity_info
45+
elif type_name == "AddressInfo":
46+
return mock_address_info
47+
return MagicMock()
48+
49+
mock_google_ads_client.get_type.side_effect = get_type_side_effect
50+
51+
# Call the main function
52+
main(
53+
mock_google_ads_client,
54+
MOCK_CUSTOMER_ID,
55+
MOCK_CAMPAIGN_ID,
56+
MOCK_KEYWORD_TEXT,
57+
MOCK_LOCATION_ID,
58+
)
59+
60+
# Assert that mutate_campaign_criteria was called
61+
mock_campaign_criterion_service.mutate_campaign_criteria.assert_called_once()
62+
63+
# Get the arguments passed to mutate_campaign_criteria
64+
args, kwargs = mock_campaign_criterion_service.mutate_campaign_criteria.call_args
65+
66+
self.assertEqual(kwargs["customer_id"], MOCK_CUSTOMER_ID)
67+
operations = kwargs["operations"]
68+
self.assertEqual(len(operations), 3) # Location, Negative Keyword, Proximity
69+
70+
# Expected campaign resource name format
71+
expected_campaign_rname = mock_google_ads_client.get_service(
72+
"CampaignService"
73+
).campaign_path(MOCK_CUSTOMER_ID, MOCK_CAMPAIGN_ID)
74+
75+
76+
# --- Verify Location Operation ---
77+
location_operation = operations[0]
78+
self.assertEqual(location_operation.create.campaign, expected_campaign_rname)
79+
# LocationInfo is set directly on the criterion, not via client.get_type in the script for create
80+
# We need to access it from the actual operation object passed to the service
81+
created_location_criterion = location_operation.create
82+
self.assertEqual(
83+
created_location_criterion.location.geo_target_constant,
84+
mock_google_ads_client.get_service(
85+
"GeoTargetConstantService"
86+
).geo_target_constant_path(MOCK_LOCATION_ID),
87+
)
88+
89+
# --- Verify Negative Keyword Operation ---
90+
keyword_operation = operations[1]
91+
self.assertEqual(keyword_operation.create.campaign, expected_campaign_rname)
92+
created_keyword_criterion = keyword_operation.create
93+
self.assertTrue(created_keyword_criterion.negative)
94+
self.assertEqual(created_keyword_criterion.keyword.text, MOCK_KEYWORD_TEXT)
95+
# Access KeywordMatchTypeEnum from the GoogleAdsClient's enums
96+
KeywordMatchTypeEnum = mock_google_ads_client.enums.KeywordMatchTypeEnum
97+
self.assertEqual(created_keyword_criterion.keyword.match_type, KeywordMatchTypeEnum.BROAD)
98+
99+
# --- Verify Proximity Operation ---
100+
proximity_operation = operations[2]
101+
self.assertEqual(proximity_operation.create.campaign, expected_campaign_rname)
102+
created_proximity_criterion = proximity_operation.create
103+
104+
# AddressInfo
105+
self.assertEqual(created_proximity_criterion.proximity.address.street_address, "38 avenue de l'Opera")
106+
self.assertEqual(created_proximity_criterion.proximity.address.city_name, "Paris")
107+
self.assertEqual(created_proximity_criterion.proximity.address.postal_code, "75002")
108+
self.assertEqual(created_proximity_criterion.proximity.address.country_code, "FR")
109+
110+
# Radius
111+
self.assertEqual(created_proximity_criterion.proximity.radius, 10.0)
112+
# Access ProximityRadiusUnitsEnum from the GoogleAdsClient's enums
113+
ProximityRadiusUnitsEnum = mock_google_ads_client.enums.ProximityRadiusUnitsEnum
114+
self.assertEqual(created_proximity_criterion.proximity.radius_units, ProximityRadiusUnitsEnum.MILES)
115+
116+
117+
if __name__ == "__main__":
118+
unittest.main()
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import unittest
2+
from unittest.mock import MagicMock, patch, call
3+
import sys
4+
5+
# Add the examples directory to the system path to import the script
6+
sys.path.append(".")
7+
8+
from examples.targeting.add_customer_negative_criteria import main
9+
10+
class TestAddCustomerNegativeCriteria(unittest.TestCase):
11+
@patch("examples.targeting.add_customer_negative_criteria.GoogleAdsClient.load_from_storage")
12+
def test_main_function_calls(self, mock_load_client):
13+
# Mock customer ID
14+
MOCK_CUSTOMER_ID = "1234567890"
15+
16+
# Create a mock GoogleAdsClient
17+
mock_google_ads_client = MagicMock()
18+
mock_load_client.return_value = mock_google_ads_client
19+
20+
# Mock the CustomerNegativeCriterionService
21+
mock_customer_negative_criterion_service = MagicMock()
22+
mock_google_ads_client.get_service.return_value = mock_customer_negative_criterion_service
23+
24+
# Mock the get_type method for CustomerNegativeCriterionOperation
25+
# In the script, client.get_type is called for "CustomerNegativeCriterionOperation"
26+
# and then attributes like "create.content_label.type_" or "create.placement.url" are set.
27+
# We need to ensure that when get_type("CustomerNegativeCriterionOperation") is called,
28+
# the returned mock can handle further attribute assignments for "create".
29+
30+
def get_type_side_effect(type_name):
31+
if type_name == "CustomerNegativeCriterionOperation":
32+
# Return a new MagicMock each time to ensure operations are distinct
33+
# and allow "create" attribute to be set on it.
34+
mock_operation = MagicMock()
35+
# mock_operation.create = MagicMock() # This might be too early or too specific
36+
return mock_operation
37+
return MagicMock() # Default for other types if any
38+
39+
mock_google_ads_client.get_type.side_effect = get_type_side_effect
40+
41+
# Mock enums that are accessed from the client instance
42+
ContentLabelTypeEnum = mock_google_ads_client.enums.ContentLabelTypeEnum
43+
44+
# Call the main function
45+
main(mock_google_ads_client, MOCK_CUSTOMER_ID)
46+
47+
# Assert that mutate_customer_negative_criteria was called
48+
mock_customer_negative_criterion_service.mutate_customer_negative_criteria.assert_called_once()
49+
50+
# Get the arguments passed to mutate_customer_negative_criteria
51+
args, kwargs = mock_customer_negative_criterion_service.mutate_customer_negative_criteria.call_args
52+
53+
self.assertEqual(kwargs["customer_id"], MOCK_CUSTOMER_ID)
54+
operations = kwargs["operations"]
55+
self.assertEqual(len(operations), 2) # Content Label and Placement
56+
57+
# --- Verify Content Label Operation ---
58+
content_label_operation = operations[0]
59+
# The criterion is directly on the operation's "create" attribute
60+
created_content_label_criterion = content_label_operation.create
61+
self.assertEqual(
62+
created_content_label_criterion.content_label.type_,
63+
ContentLabelTypeEnum.TRAGEDY,
64+
)
65+
66+
# --- Verify Placement Operation ---
67+
placement_operation = operations[1]
68+
created_placement_criterion = placement_operation.create
69+
self.assertEqual(
70+
created_placement_criterion.placement.url, "http://www.example.com"
71+
)
72+
73+
if __name__ == "__main__":
74+
unittest.main()
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import unittest
2+
from unittest.mock import MagicMock, patch, call
3+
import sys
4+
5+
# Add the examples directory to the system path to import the script
6+
sys.path.append(".")
7+
8+
from examples.targeting.add_demographic_targeting_criteria import main
9+
10+
class TestAddDemographicTargetingCriteria(unittest.TestCase):
11+
@patch("examples.targeting.add_demographic_targeting_criteria.GoogleAdsClient.load_from_storage")
12+
def test_main_function_calls(self, mock_load_client):
13+
# Mock IDs
14+
MOCK_CUSTOMER_ID = "1234567890"
15+
MOCK_AD_GROUP_ID = "9876543210"
16+
17+
# Create a mock GoogleAdsClient
18+
mock_google_ads_client = MagicMock()
19+
mock_load_client.return_value = mock_google_ads_client
20+
21+
# Mock the AdGroupCriterionService
22+
mock_ad_group_criterion_service = MagicMock()
23+
24+
# Mock the AdGroupService (for ad_group_path)
25+
mock_ad_group_service = MagicMock()
26+
27+
def get_service_side_effect(service_name, version=None):
28+
if service_name == "AdGroupCriterionService":
29+
return mock_ad_group_criterion_service
30+
elif service_name == "AdGroupService":
31+
return mock_ad_group_service
32+
return MagicMock()
33+
34+
mock_google_ads_client.get_service.side_effect = get_service_side_effect
35+
36+
# Expected ad_group resource name
37+
expected_ad_group_rname = mock_ad_group_service.ad_group_path(
38+
MOCK_CUSTOMER_ID, MOCK_AD_GROUP_ID
39+
)
40+
41+
# Mock the get_type method for AdGroupCriterionOperation
42+
def get_type_side_effect(type_name):
43+
if type_name == "AdGroupCriterionOperation":
44+
# Return a new MagicMock each time for distinct operations
45+
return MagicMock()
46+
return MagicMock()
47+
48+
mock_google_ads_client.get_type.side_effect = get_type_side_effect
49+
50+
# Mock enums
51+
GenderTypeEnum = mock_google_ads_client.enums.GenderTypeEnum
52+
AgeRangeTypeEnum = mock_google_ads_client.enums.AgeRangeTypeEnum
53+
54+
# Call the main function
55+
main(mock_google_ads_client, MOCK_CUSTOMER_ID, MOCK_AD_GROUP_ID)
56+
57+
# Assert that mutate_ad_group_criteria was called
58+
mock_ad_group_criterion_service.mutate_ad_group_criteria.assert_called_once()
59+
60+
# Get the arguments passed to mutate_ad_group_criteria
61+
args, kwargs = mock_ad_group_criterion_service.mutate_ad_group_criteria.call_args
62+
63+
self.assertEqual(kwargs["customer_id"], MOCK_CUSTOMER_ID)
64+
operations = kwargs["operations"]
65+
self.assertEqual(len(operations), 2) # Gender and Age Range
66+
67+
# --- Verify Gender Criterion Operation ---
68+
gender_operation = operations[0]
69+
created_gender_criterion = gender_operation.create
70+
self.assertEqual(created_gender_criterion.ad_group, expected_ad_group_rname)
71+
self.assertEqual(created_gender_criterion.gender.type_, GenderTypeEnum.MALE)
72+
# In the script, gender is positive targeting, so .negative should not be True
73+
# A non-existent attribute or one that is False would be fine.
74+
# We'll check it's not explicitly True. If it's not set, getattr will raise AttributeError.
75+
# If it's set to False, this check will pass.
76+
self.assertNotEqual(getattr(created_gender_criterion, 'negative', False), True)
77+
78+
79+
# --- Verify Age Range Criterion Operation (Negative) ---
80+
age_range_operation = operations[1]
81+
created_age_range_criterion = age_range_operation.create
82+
self.assertEqual(created_age_range_criterion.ad_group, expected_ad_group_rname)
83+
self.assertEqual(created_age_range_criterion.age_range.type_, AgeRangeTypeEnum.AGE_RANGE_18_24)
84+
self.assertTrue(created_age_range_criterion.negative)
85+
86+
87+
if __name__ == "__main__":
88+
unittest.main()
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import unittest
2+
from unittest.mock import MagicMock, patch, call
3+
import sys
4+
5+
# Add the examples directory to the system path to import the script
6+
sys.path.append(".")
7+
8+
from examples.targeting.get_geo_target_constants_by_names import main
9+
10+
class TestGetGeoTargetConstantsByNames(unittest.TestCase):
11+
@patch("examples.targeting.get_geo_target_constants_by_names.GoogleAdsClient.load_from_storage")
12+
def test_main_function_calls(self, mock_load_client):
13+
# Create a mock GoogleAdsClient
14+
mock_google_ads_client = MagicMock()
15+
mock_load_client.return_value = mock_google_ads_client
16+
17+
# Mock the GeoTargetConstantService
18+
mock_geo_target_constant_service = MagicMock()
19+
mock_google_ads_client.get_service.return_value = mock_geo_target_constant_service
20+
21+
# Mock the GeoTargetConstantService
22+
mock_geo_target_constant_service = MagicMock()
23+
mock_google_ads_client.get_service.return_value = mock_geo_target_constant_service
24+
25+
# Mock the SuggestGeoTargetConstantsRequest object that client.get_type will return.
26+
# client.get_type("SuggestGeoTargetConstantsRequest") returns an instance, not a class.
27+
mock_request_instance = MagicMock(name="SuggestGeoTargetConstantsRequestInstance")
28+
mock_google_ads_client.get_type.return_value = mock_request_instance
29+
30+
# Set up the nested structure for location_names.names on this instance.
31+
# The script accesses request.location_names.names
32+
mock_location_names_object = MagicMock(name="LocationNamesObject")
33+
mock_request_instance.location_names = mock_location_names_object
34+
# Initialize .names as a list on this mock_location_names_object because the script appends to it.
35+
mock_location_names_object.names = []
36+
37+
38+
# Mock the response from suggest_geo_target_constants
39+
# This should be an iterable where each item has the attributes accessed in the script's loop
40+
mock_suggestion_1 = MagicMock()
41+
mock_suggestion_1.resource_name = "geoTargetConstants/1000"
42+
mock_suggestion_1.name = "Paris"
43+
mock_suggestion_1.country_code = "FR"
44+
mock_suggestion_1.target_type = "City"
45+
mock_suggestion_1.status = "ENABLED"
46+
47+
mock_suggestion_2 = MagicMock()
48+
mock_suggestion_2.resource_name = "geoTargetConstants/2000"
49+
mock_suggestion_2.name = "Quebec"
50+
mock_suggestion_2.country_code = "CA"
51+
mock_suggestion_2.target_type = "Province"
52+
mock_suggestion_2.status = "ENABLED"
53+
54+
# Mock the response from suggest_geo_target_constants
55+
mock_response = MagicMock()
56+
mock_response.geo_target_constant_suggestions = [
57+
mock_suggestion_1, mock_suggestion_2
58+
]
59+
mock_geo_target_constant_service.suggest_geo_target_constants.return_value = mock_response
60+
61+
# Call the main function
62+
main(mock_google_ads_client)
63+
64+
# Assert that suggest_geo_target_constants was called
65+
mock_geo_target_constant_service.suggest_geo_target_constants.assert_called_once()
66+
67+
# The first argument to suggest_geo_target_constants is the request object
68+
# In the script, this is constructed and then passed.
69+
# Our mock_request_instance should be what was passed.
70+
passed_request = mock_geo_target_constant_service.suggest_geo_target_constants.call_args[0][0]
71+
72+
# Verify the attributes of the passed request object
73+
self.assertEqual(passed_request.locale, "en")
74+
self.assertEqual(passed_request.country_code, "FR")
75+
76+
# Verify location_names.names
77+
# The script does: request.location_names.names.extend(["Paris", "Quebec", "Spain", "Deutschland"])
78+
# We check the final state of this list on mock_location_names_object.names
79+
self.assertListEqual(
80+
mock_location_names_object.names,
81+
["Paris", "Quebec", "Spain", "Deutschland"]
82+
)
83+
84+
# Also, ensure the request object passed to the service call is the one we configured
85+
self.assertEqual(passed_request, mock_request_instance)
86+
87+
88+
if __name__ == "__main__":
89+
unittest.main()

0 commit comments

Comments
 (0)