Skip to content

Commit 7f227ca

Browse files
feat: Add initial tests for remarketing examples
Implements unit tests for several scripts in the examples/remarketing directory, focusing on v19 API compatibility. Key changes: - Created test directory examples/remarketing/tests/. - Added tests for add_conversion_action.py, including a fix for an import path in the script itself. - Added comprehensive tests for upload_offline_conversion.py, utilizing PropertyMock to verify attribute assignments on mock objects. Covers GCLID, GBRAID, WBRAID, custom variables, order ID, and consent. - Added tests for add_customer_match_user_list.py covering: - normalize_and_hash() (fixed a bug in the script). - create_customer_match_user_list() (fixed imports in the script). - build_offline_user_data_job_operations(). Worked around MagicMock state reflection issues for AddressInfo by focusing on verifying calls to normalize_and_hash and output structure. - add_users_to_customer_match_user_list() (scenario: new job, do not run). Resolved import scoping issues for enums in tests. Challenges: - MagicMock state reflection for nested/dynamically assigned attributes required workarounds (PropertyMock or focusing on input/output of helper functions). - Resolved some import scoping issues within test methods. - Fixed NameError for 'call' by adding the correct import.
1 parent 81fdaff commit 7f227ca

File tree

6 files changed

+835
-8
lines changed

6 files changed

+835
-8
lines changed

examples/remarketing/add_conversion_action.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@
2222

2323
from google.ads.googleads.client import GoogleAdsClient
2424
from google.ads.googleads.errors import GoogleAdsException
25+
# Corrected import path for ConversionActionServiceClient
26+
from google.ads.googleads.v19.services.services.conversion_action_service.client import (
27+
ConversionActionServiceClient,
28+
)
29+
# ConversionActionOperation is correctly in the .types module
2530
from google.ads.googleads.v19.services.types.conversion_action_service import (
2631
ConversionActionOperation,
27-
ConversionActionServiceClient,
2832
)
2933
from google.ads.googleads.v19.resources.types.conversion_action import (
3034
ConversionAction,

examples/remarketing/add_customer_match_user_list.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,30 @@
3434

3535
from google.ads.googleads.client import GoogleAdsClient
3636
from google.ads.googleads.errors import GoogleAdsException
37-
from google.ads.googleads.v19.services.types.google_ads_service import (
37+
# Corrected import path for GoogleAdsServiceClient
38+
from google.ads.googleads.v19.services.services.google_ads_service.client import (
3839
GoogleAdsServiceClient,
40+
)
41+
# SearchGoogleAdsStreamResponse is correctly located in .types
42+
from google.ads.googleads.v19.services.types.google_ads_service import (
3943
SearchGoogleAdsStreamResponse,
4044
)
45+
# Corrected import for UserListServiceClient
46+
from google.ads.googleads.v19.services.services.user_list_service.client import (
47+
UserListServiceClient,
48+
)
49+
# UserListOperation is correctly in .types
4150
from google.ads.googleads.v19.services.types.user_list_service import (
4251
UserListOperation,
43-
UserListServiceClient,
4452
)
4553
from google.ads.googleads.v19.resources.types.user_list import UserList
54+
# Corrected import for OfflineUserDataJobServiceClient
55+
from google.ads.googleads.v19.services.services.offline_user_data_job_service.client import (
56+
OfflineUserDataJobServiceClient,
57+
)
58+
# OfflineUserDataJobOperation and AddOfflineUserDataJobOperationsResponse are correctly in .types
4659
from google.ads.googleads.v19.services.types.offline_user_data_job_service import (
4760
OfflineUserDataJobOperation,
48-
OfflineUserDataJobServiceClient,
4961
AddOfflineUserDataJobOperationsResponse,
5062
)
5163
from google.ads.googleads.v19.resources.types.offline_user_data_job import (
@@ -54,8 +66,10 @@
5466
from google.ads.googleads.v19.common.types.offline_user_data import (
5567
UserData,
5668
UserIdentifier,
57-
AddressInfo,
69+
# AddressInfo is not here
5870
)
71+
# Corrected import for AddressInfo
72+
from google.ads.googleads.v19.common.types.criteria import AddressInfo
5973

6074

6175
def main(
@@ -575,14 +589,15 @@ def normalize_and_hash(s: str, remove_all_whitespace: bool) -> str:
575589
Returns:
576590
A normalized (lowercase, remove whitespace) and SHA-256 hashed string.
577591
"""
578-
# Normalizes by first converting all characters to lowercase, then trimming
579-
# spaces.
592+
# Normalizes by first converting all characters to lowercase.
593+
s = s.lower()
594+
580595
if remove_all_whitespace:
581596
# Removes leading, trailing, and intermediate whitespace.
582597
s = "".join(s.split())
583598
else:
584599
# Removes only leading and trailing spaces.
585-
s = s.strip().lower()
600+
s = s.strip() # Lowercasing is already done.
586601

587602
# Hashes the normalized string using the hashing algorithm.
588603
return hashlib.sha256(s.encode()).hexdigest()

examples/remarketing/tests/__init__.py

Whitespace-only changes.
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import unittest
2+
from unittest.mock import MagicMock, patch
3+
4+
from google.ads.googleads.client import GoogleAdsClient
5+
# Corrected version to v19
6+
from google.ads.googleads.v19.enums import (
7+
ConversionActionCategoryEnum,
8+
ConversionActionStatusEnum,
9+
ConversionActionTypeEnum,
10+
)
11+
# ConversionOriginEnum was not used in the original script or the test logic for it.
12+
# from google.ads.googleads.v19.enums import ConversionOriginEnum
13+
from google.ads.googleads.v19.resources.types.conversion_action import (
14+
ConversionAction,
15+
)
16+
# Corrected import path for ConversionActionServiceClient based on grep results
17+
from google.ads.googleads.v19.services.services.conversion_action_service.client import (
18+
ConversionActionServiceClient,
19+
)
20+
# ConversionActionOperation is a type and should be imported from the types module
21+
from google.ads.googleads.v19.services.types.conversion_action_service import (
22+
ConversionActionOperation,
23+
)
24+
25+
26+
# Import the main function from the module to be tested
27+
from examples.remarketing.add_conversion_action import main
28+
29+
30+
class TestAddConversionAction(unittest.TestCase):
31+
@patch("examples.remarketing.add_conversion_action.uuid.uuid4")
32+
@patch("google.ads.googleads.client.GoogleAdsClient.load_from_storage")
33+
def test_main_function_calls(
34+
self, mock_load_from_storage, mock_uuid4
35+
):
36+
# Configure the mock GoogleAdsClient
37+
mock_google_ads_client = MagicMock(spec=GoogleAdsClient)
38+
mock_google_ads_client.api_version = "v19" # Corrected API version
39+
mock_load_from_storage.return_value = mock_google_ads_client
40+
41+
# Mock the enums used by the script
42+
# We provide mocks that return the integer values expected by protobuf messages
43+
mock_conversion_action_type_enum = MagicMock()
44+
mock_conversion_action_type_enum.UPLOAD_CLICKS = 7 # As per .proto definition
45+
46+
mock_conversion_action_category_enum = MagicMock()
47+
mock_conversion_action_category_enum.DEFAULT = 4 # DEFAULT = 4 for ConversionActionCategory
48+
49+
mock_conversion_action_status_enum = MagicMock()
50+
mock_conversion_action_status_enum.ENABLED = 2 # ENABLED = 2 for ConversionActionStatus
51+
52+
mock_enums_obj = MagicMock()
53+
mock_enums_obj.ConversionActionTypeEnum = mock_conversion_action_type_enum
54+
mock_enums_obj.ConversionActionCategoryEnum = mock_conversion_action_category_enum
55+
mock_enums_obj.ConversionActionStatusEnum = mock_conversion_action_status_enum
56+
mock_google_ads_client.enums = mock_enums_obj
57+
58+
# Configure the mock ConversionActionService
59+
mock_conversion_action_service = MagicMock(
60+
spec=ConversionActionServiceClient # Corrected spec
61+
)
62+
mock_google_ads_client.get_service.return_value = (
63+
mock_conversion_action_service
64+
)
65+
66+
# Mock the get_type method for ConversionActionOperation and other types
67+
def mock_get_type(type_name, **kwargs): # Add **kwargs to handle potential version args
68+
if type_name == "ConversionActionOperation":
69+
mock_op = MagicMock()
70+
# The 'create' field in ConversionActionOperation is of type ConversionAction.
71+
# We need to ensure this mock can be assigned to operation.create
72+
mock_op.create = mock_google_ads_client.get_type("ConversionAction")
73+
return mock_op
74+
elif type_name == "ConversionAction":
75+
# This will be assigned to operation.create
76+
mock_action = MagicMock(spec=ConversionAction)
77+
# Explicitly create the value_settings attribute on the mock ConversionAction,
78+
# as it will be accessed and modified by the script.
79+
mock_action.value_settings = mock_google_ads_client.get_type("ValueSettings")
80+
return mock_action
81+
elif type_name == "ValueSettings":
82+
# This mock will be assigned to mock_action.value_settings
83+
return MagicMock()
84+
elif type_name == "AttributionModelSettings": # Not used by current script
85+
return MagicMock()
86+
# Add other types if needed by the script
87+
return MagicMock()
88+
89+
mock_google_ads_client.get_type.side_effect = mock_get_type
90+
91+
# Mock uuid.uuid4() to return a fixed UUID
92+
mock_uuid4.return_value = "test-uuid"
93+
94+
# Define a dummy customer ID
95+
customer_id = "1234567890"
96+
97+
# Call the main function
98+
main(mock_google_ads_client, customer_id)
99+
100+
# Assert that get_service was called for "ConversionActionService"
101+
mock_google_ads_client.get_service.assert_any_call(
102+
"ConversionActionService"
103+
)
104+
105+
# Assert that mutate_conversion_actions was called once
106+
self.assertEqual(
107+
mock_conversion_action_service.mutate_conversion_actions.call_count,
108+
1,
109+
)
110+
111+
# Get the arguments passed to mutate_conversion_actions
112+
call_args = (
113+
mock_conversion_action_service.mutate_conversion_actions.call_args
114+
)
115+
116+
# The method is called with keyword arguments in the script
117+
# call_args.args would be empty, call_args.kwargs would have the arguments
118+
self.assertFalse(call_args.args, "mutate_conversion_actions should be called with keyword arguments, not positional ones.")
119+
self.assertTrue(call_args.kwargs, "mutate_conversion_actions should be called with keyword arguments.")
120+
121+
passed_customer_id = call_args.kwargs.get('customer_id')
122+
passed_operations = call_args.kwargs.get('operations')
123+
124+
self.assertEqual(passed_customer_id, customer_id)
125+
self.assertIsNotNone(passed_operations, "Operations list was not passed as a keyword argument.")
126+
self.assertEqual(len(passed_operations), 1)
127+
128+
# Get the operation and the created conversion action
129+
operation = passed_operations[0]
130+
# The 'create' attribute is set on the operation object itself by get_type mock
131+
created_action = operation.create
132+
133+
# Assertions for the ConversionAction attributes
134+
self.assertEqual(
135+
created_action.name, "Earth to Mars Cruises Conversion test-uuid"
136+
)
137+
# Assertions for the ConversionAction attributes using integer values
138+
self.assertEqual(
139+
created_action.type_,
140+
7, # Corresponds to UPLOAD_CLICKS
141+
)
142+
self.assertEqual(
143+
created_action.category,
144+
4, # Corresponds to DEFAULT for ConversionActionCategory
145+
)
146+
self.assertEqual(
147+
created_action.status,
148+
2, # Corresponds to ENABLED for ConversionActionStatus
149+
)
150+
# For ValueSettings, check if it was set (it's an object)
151+
self.assertIsNotNone(created_action.value_settings)
152+
# Check specific attributes of value_settings
153+
# The script being tested sets these:
154+
# value_settings.default_value = 15.0
155+
# value_settings.always_use_default_value = True
156+
self.assertEqual(created_action.value_settings.default_value, 15.0)
157+
self.assertTrue(created_action.value_settings.always_use_default_value)
158+
159+
160+
# The attribute primary_for_goal is not explicitly set in the add_conversion_action.py script for UPLOAD_CLICKS type.
161+
# It defaults to True for many types, but it's better to assert what's explicitly set or documented as default.
162+
# For UPLOAD_CLICKS, primary_for_goal is not set in the script.
163+
# Let's remove this assertion if it's not set, or verify default behavior if critical.
164+
# After checking the ConversionAction resource type, primary_for_goal is a boolean.
165+
# If the script doesn't set it, it will take on its default value (often True if not specified).
166+
# The example script does not set primary_for_goal.
167+
# self.assertTrue(created_action.primary_for_goal)
168+
# Let's verify if the mock handles this. If it's not set, it won't be on the mock unless spec is very strict or default values are part of mock.
169+
# For now, let's remove this line, as it's not explicitly set in the script.
170+
171+
172+
if __name__ == "__main__":
173+
unittest.main()

0 commit comments

Comments
 (0)