Skip to content

Commit 9a57e6f

Browse files
Add tests for advanced_operations examples
This change adds tests for the following scripts in the examples/advanced_operations directory: - add_call_ad.py - add_display_upload_ad.py - add_dynamic_search_ads.py - add_performance_max_campaign.py - add_responsive_search_ad_full.py The tests cover the main functionality of each script, including successful execution paths and error handling for GoogleAdsException. All new and existing tests pass.
1 parent 15643ea commit 9a57e6f

File tree

5 files changed

+1068
-0
lines changed

5 files changed

+1068
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/env python
2+
# Copyright 2022 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Tests for add_call_ad."""
16+
17+
import argparse
18+
import unittest
19+
from unittest import mock
20+
21+
from examples.advanced_operations.add_call_ad import main
22+
from google.ads.googleads.client import GoogleAdsClient
23+
from google.ads.googleads.errors import GoogleAdsException
24+
25+
26+
class AddCallAdTest(unittest.TestCase):
27+
"""Tests for the add_call_ad example."""
28+
29+
@mock.patch("examples.advanced_operations.add_call_ad.GoogleAdsClient.load_from_storage")
30+
def test_main(self, mock_load_from_storage):
31+
"""Tests the main function with mock arguments."""
32+
# Create a mock GoogleAdsClient instance
33+
mock_client = mock.Mock(spec=GoogleAdsClient)
34+
# Add the enums attribute to the mock_client
35+
mock_client.enums = mock.Mock()
36+
# Configure the mock_load_from_storage to return the mock_client
37+
mock_load_from_storage.return_value = mock_client
38+
39+
# Create mock services
40+
mock_googleads_service = mock.Mock()
41+
mock_ad_group_ad_service = mock.Mock()
42+
mock_client.get_service.side_effect = [
43+
mock_googleads_service,
44+
mock_ad_group_ad_service,
45+
]
46+
47+
# Mock the AdGroupAdOperation
48+
mock_operation = mock.Mock()
49+
mock_client.get_type.return_value = mock_operation
50+
51+
# Mock the mutate_ad_group_ads response
52+
mock_mutate_response = mock.Mock()
53+
mock_mutate_response.results = [mock.Mock()]
54+
mock_mutate_response.results[0].resource_name = "TestResourceName"
55+
mock_ad_group_ad_service.mutate_ad_group_ads.return_value = (
56+
mock_mutate_response
57+
)
58+
59+
# Mock command-line arguments
60+
mock_args = argparse.Namespace(
61+
customer_id="1234567890",
62+
ad_group_id="0987654321",
63+
phone_number="(800) 555-0100",
64+
phone_country="US",
65+
conversion_action_id="1122334455",
66+
)
67+
68+
# Call the main function by simulating command-line execution
69+
with mock.patch("sys.argv", [
70+
"add_call_ad.py",
71+
"-c", mock_args.customer_id,
72+
"-a", mock_args.ad_group_id,
73+
"-n", mock_args.phone_number,
74+
"-p", mock_args.phone_country,
75+
"-v", mock_args.conversion_action_id,
76+
]), mock.patch(
77+
"examples.advanced_operations.add_call_ad.argparse.ArgumentParser"
78+
) as mock_argparse:
79+
# Mock parse_args to return the predefined mock_args
80+
mock_argparse_instance = mock_argparse.return_value
81+
mock_argparse_instance.parse_args.return_value = mock_args
82+
83+
# Import and run the main function from the script's __main__ block
84+
from examples.advanced_operations import add_call_ad
85+
86+
# We need to reload the module to ensure that the main function
87+
# is executed in the context of the test case with the mocked objects.
88+
# import importlib
89+
# importlib.reload(add_call_ad)
90+
import runpy
91+
runpy.run_module("examples.advanced_operations.add_call_ad", run_name="__main__")
92+
93+
# Assert that the necessary methods were called
94+
mock_load_from_storage.assert_called_once_with(version="v20")
95+
mock_client.get_service.assert_any_call("GoogleAdsService")
96+
mock_client.get_service.assert_any_call("AdGroupAdService")
97+
mock_client.get_type.assert_called_once_with("AdGroupAdOperation")
98+
mock_ad_group_ad_service.mutate_ad_group_ads.assert_called_once()
99+
100+
@mock.patch("examples.advanced_operations.add_call_ad.GoogleAdsClient.load_from_storage")
101+
def test_main_google_ads_exception(self, mock_load_from_storage):
102+
"""Tests the main function when a GoogleAdsException is raised."""
103+
# Create a mock GoogleAdsClient instance
104+
mock_client = mock.Mock(spec=GoogleAdsClient)
105+
# Add the enums attribute to the mock_client
106+
mock_client.enums = mock.Mock()
107+
# Configure the mock_load_from_storage to return the mock_client
108+
mock_load_from_storage.return_value = mock_client
109+
110+
# Mock services to raise GoogleAdsException
111+
mock_googleads_service = mock.Mock()
112+
mock_ad_group_ad_service = mock.Mock()
113+
mock_client.get_service.side_effect = [
114+
mock_googleads_service,
115+
mock_ad_group_ad_service,
116+
]
117+
# Configure the mock to raise GoogleAdsException
118+
mock_error = mock.Mock()
119+
mock_error.code.return_value.name = "TestError"
120+
# Create a mock failure object with an errors attribute
121+
mock_failure = mock.Mock()
122+
# Each error in ex.failure.errors should have a message attribute
123+
# and optionally a location attribute.
124+
mock_error_detail = mock.Mock()
125+
mock_error_detail.message = "Test error message."
126+
mock_error_detail.location.field_path_elements = [] # No field path elements for simplicity
127+
mock_failure.errors = [mock_error_detail]
128+
exception_to_raise = GoogleAdsException(
129+
mock_error,
130+
"Dummy failure message that will be overwritten",
131+
"Google Ads API request failed.",
132+
request_id="test_request_id"
133+
)
134+
# Explicitly set the failure attribute to be the mock_failure object
135+
exception_to_raise.failure = mock_failure
136+
mock_ad_group_ad_service.mutate_ad_group_ads.side_effect = exception_to_raise
137+
138+
# Mock command-line arguments
139+
mock_args = argparse.Namespace(
140+
customer_id="1234567890",
141+
ad_group_id="0987654321",
142+
phone_number="(800) 555-0100",
143+
phone_country="US",
144+
conversion_action_id="1122334455",
145+
)
146+
147+
# Call the main function by simulating command-line execution
148+
# and assert that it exits with code 1
149+
with mock.patch("sys.argv", [
150+
"add_call_ad.py",
151+
"-c", mock_args.customer_id,
152+
"-a", mock_args.ad_group_id,
153+
"-n", mock_args.phone_number,
154+
"-p", mock_args.phone_country,
155+
"-v", mock_args.conversion_action_id,
156+
]), mock.patch(
157+
"examples.advanced_operations.add_call_ad.argparse.ArgumentParser"
158+
) as mock_argparse:
159+
# Mock parse_args to return the predefined mock_args
160+
mock_argparse_instance = mock_argparse.return_value
161+
mock_argparse_instance.parse_args.return_value = mock_args
162+
163+
# Import and run the main function from the script's __main__ block
164+
from examples.advanced_operations import add_call_ad
165+
166+
import runpy
167+
with self.assertRaises(SystemExit) as cm:
168+
runpy.run_module("examples.advanced_operations.add_call_ad", run_name="__main__")
169+
170+
self.assertEqual(cm.exception.code, 1)
171+
172+
173+
if __name__ == "__main__":
174+
unittest.main()
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env python
2+
# Copyright 2024 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Tests for add_display_upload_ad."""
16+
17+
import argparse
18+
import unittest
19+
from unittest import mock
20+
import runpy
21+
22+
from google.ads.googleads.client import GoogleAdsClient
23+
from google.ads.googleads.errors import GoogleAdsException
24+
25+
26+
# Define mock constants for resource names
27+
_MOCK_ASSET_RESOURCE_NAME = "customers/12345/assets/67890"
28+
_MOCK_AD_GROUP_AD_RESOURCE_NAME = "customers/12345/adGroupAds/24680"
29+
30+
31+
class AddDisplayUploadAdTest(unittest.TestCase):
32+
"""Tests for the add_display_upload_ad example."""
33+
34+
@mock.patch("examples.advanced_operations.add_display_upload_ad.requests.get")
35+
@mock.patch("examples.advanced_operations.add_display_upload_ad.GoogleAdsClient.load_from_storage")
36+
def test_main_success(self, mock_load_from_storage, mock_requests_get):
37+
"""Tests the main function runs successfully with mock arguments."""
38+
mock_client = mock.Mock(spec=GoogleAdsClient)
39+
mock_load_from_storage.return_value = mock_client
40+
mock_client.enums = mock.Mock() # For AdGroupAdStatusEnum, AssetTypeEnum, etc.
41+
42+
# Mock requests.get() to return a mock response with content
43+
mock_response = mock.Mock()
44+
mock_response.content = b"dummy_zip_content"
45+
mock_requests_get.return_value = mock_response
46+
47+
# Mock AssetService
48+
mock_asset_service = mock.Mock()
49+
mock_client.get_service.return_value = mock_asset_service
50+
mock_asset_operation = mock.Mock()
51+
mock_client.get_type.return_value = mock_asset_operation
52+
mock_mutate_assets_response = mock.Mock()
53+
# Ensure results is a list containing a mock object
54+
mock_asset_result = mock.Mock()
55+
mock_asset_result.resource_name = _MOCK_ASSET_RESOURCE_NAME
56+
mock_mutate_assets_response.results = [mock_asset_result]
57+
mock_asset_service.mutate_assets.return_value = mock_mutate_assets_response
58+
59+
# Mock AdGroupAdService & AdGroupService (for ad_group_path)
60+
mock_ad_group_ad_service = mock.Mock()
61+
mock_ad_group_service = mock.Mock()
62+
63+
def get_service_side_effect(service_name, version=None):
64+
if service_name == "AssetService":
65+
return mock_asset_service
66+
elif service_name == "AdGroupAdService":
67+
return mock_ad_group_ad_service
68+
elif service_name == "AdGroupService": # For ad_group_path
69+
return mock_ad_group_service
70+
raise ValueError(f"Unexpected service: {service_name}")
71+
mock_client.get_service.side_effect = get_service_side_effect
72+
73+
mock_ad_group_path = "customers/12345/adGroups/09876"
74+
mock_ad_group_service.ad_group_path.return_value = mock_ad_group_path
75+
76+
mock_ad_group_ad_operation = mock.Mock()
77+
# Need to make get_type return a new mock for the second call
78+
# The first call to get_type is for AssetOperation
79+
# The second call to get_type is for AdGroupAdOperation
80+
# Use mock_client to get the type, not the undefined 'client'
81+
mock_client.get_type.side_effect = [mock_client.get_type("AssetOperation"), mock_ad_group_ad_operation]
82+
83+
84+
mock_mutate_ad_group_ads_response = mock.Mock()
85+
# Ensure results is a list containing a mock object
86+
mock_ad_group_ad_result = mock.Mock()
87+
mock_ad_group_ad_result.resource_name = _MOCK_AD_GROUP_AD_RESOURCE_NAME
88+
mock_mutate_ad_group_ads_response.results = [mock_ad_group_ad_result]
89+
mock_ad_group_ad_service.mutate_ad_group_ads.return_value = mock_mutate_ad_group_ads_response
90+
91+
# Mock command-line arguments
92+
mock_args = argparse.Namespace(
93+
customer_id="1234567890",
94+
ad_group_id="0987654321",
95+
)
96+
97+
with mock.patch("sys.argv", [
98+
"add_display_upload_ad.py",
99+
"-c", mock_args.customer_id,
100+
"-a", str(mock_args.ad_group_id), # ad_group_id is int in script
101+
]), mock.patch(
102+
"examples.advanced_operations.add_display_upload_ad.argparse.ArgumentParser"
103+
) as mock_argparse:
104+
mock_argparse.return_value.parse_args.return_value = mock_args
105+
runpy.run_module("examples.advanced_operations.add_display_upload_ad", run_name="__main__")
106+
107+
mock_load_from_storage.assert_called_once_with(version="v20")
108+
mock_requests_get.assert_called_once_with("https://gaagl.page.link/ib87")
109+
mock_asset_service.mutate_assets.assert_called_once()
110+
mock_ad_group_ad_service.mutate_ad_group_ads.assert_called_once()
111+
# Check that the correct ad_group_path was used
112+
self.assertEqual(
113+
mock_ad_group_ad_operation.create.ad_group, mock_ad_group_path
114+
)
115+
# Check that the asset from the first call was used in the second
116+
self.assertEqual(
117+
mock_ad_group_ad_operation.create.ad.display_upload_ad.media_bundle.asset,
118+
_MOCK_ASSET_RESOURCE_NAME
119+
)
120+
121+
@mock.patch("examples.advanced_operations.add_display_upload_ad.requests.get")
122+
@mock.patch("examples.advanced_operations.add_display_upload_ad.GoogleAdsClient.load_from_storage")
123+
def test_main_google_ads_exception(self, mock_load_from_storage, mock_requests_get):
124+
"""Tests the main function when a GoogleAdsException is raised."""
125+
mock_client = mock.Mock(spec=GoogleAdsClient)
126+
mock_load_from_storage.return_value = mock_client
127+
mock_client.enums = mock.Mock()
128+
129+
mock_response = mock.Mock()
130+
mock_response.content = b"dummy_zip_content"
131+
mock_requests_get.return_value = mock_response
132+
133+
mock_asset_service = mock.Mock()
134+
mock_client.get_service.return_value = mock_asset_service
135+
136+
# Configure the mock to raise GoogleAdsException
137+
mock_error = mock.Mock()
138+
mock_error.code.return_value.name = "TestAssetError"
139+
mock_failure = mock.Mock()
140+
mock_error_detail = mock.Mock()
141+
mock_error_detail.message = "Test asset error message."
142+
mock_error_detail.location.field_path_elements = []
143+
mock_failure.errors = [mock_error_detail]
144+
145+
# Create the exception instance
146+
exception_to_raise = GoogleAdsException(
147+
mock_error,
148+
mock_failure, # This should be the failure object
149+
"Google Ads API request failed (assets).", # This is the message string
150+
request_id="test_asset_request_id"
151+
)
152+
# Ensure ex.failure is the mock_failure object, not the message string.
153+
# The GoogleAdsException constructor might assign the message to ex.failure
154+
# if the failure object itself is not correctly structured or if the message
155+
# parameter takes precedence in some cases.
156+
# To be safe, let's explicitly set it, similar to test_add_call_ad.
157+
exception_to_raise.failure = mock_failure
158+
mock_asset_service.mutate_assets.side_effect = exception_to_raise
159+
160+
mock_args = argparse.Namespace(
161+
customer_id="1234567890",
162+
ad_group_id="0987654321",
163+
)
164+
165+
with mock.patch("sys.argv", [
166+
"add_display_upload_ad.py",
167+
"-c", mock_args.customer_id,
168+
"-a", str(mock_args.ad_group_id),
169+
]), mock.patch(
170+
"examples.advanced_operations.add_display_upload_ad.argparse.ArgumentParser"
171+
) as mock_argparse:
172+
mock_argparse.return_value.parse_args.return_value = mock_args
173+
with self.assertRaises(SystemExit) as cm:
174+
runpy.run_module("examples.advanced_operations.add_display_upload_ad", run_name="__main__")
175+
176+
self.assertEqual(cm.exception.code, 1)
177+
178+
mock_load_from_storage.assert_called_once_with(version="v20")
179+
mock_requests_get.assert_called_once_with("https://gaagl.page.link/ib87")
180+
mock_asset_service.mutate_assets.assert_called_once()
181+
182+
183+
if __name__ == "__main__":
184+
unittest.main()

0 commit comments

Comments
 (0)