Skip to content

Commit 6ec1528

Browse files
authored
Unit tests for examples/misc (#1000)
1 parent 69a3b67 commit 6ec1528

File tree

8 files changed

+1239
-15
lines changed

8 files changed

+1239
-15
lines changed

examples/misc/__init__.py

Whitespace-only changes.

examples/misc/campaign_report_to_csv.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@
3636

3737

3838
_DEFAULT_FILE_NAME = "campaign_report_to_csv_results.csv"
39+
_QUERY: str = """
40+
SELECT
41+
customer.descriptive_name,
42+
segments.date,
43+
campaign.name,
44+
metrics.impressions,
45+
metrics.clicks,
46+
metrics.cost_micros
47+
FROM campaign
48+
WHERE
49+
segments.date DURING LAST_7_DAYS
50+
ORDER BY metrics.impressions DESC
51+
LIMIT 25"""
3952

4053

4154
def main(
@@ -55,24 +68,10 @@ def main(
5568
file_path: str = os.path.join(file_dir, output_file)
5669
ga_service = client.get_service("GoogleAdsService")
5770

58-
query: str = """
59-
SELECT
60-
customer.descriptive_name,
61-
segments.date,
62-
campaign.name,
63-
metrics.impressions,
64-
metrics.clicks,
65-
metrics.cost_micros
66-
FROM campaign
67-
WHERE
68-
segments.date DURING LAST_7_DAYS
69-
ORDER BY metrics.impressions DESC
70-
LIMIT 25"""
71-
7271
# Issues a search request using streaming.
7372
search_request = client.get_type("SearchGoogleAdsStreamRequest")
7473
search_request.customer_id = customer_id
75-
search_request.query = query
74+
search_request.query = _QUERY
7675
stream = ga_service.search_stream(search_request)
7776

7877
with open(file_path, "w", newline="") as f:

tests/examples/misc/__init__.py

Whitespace-only changes.
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import unittest
15+
from unittest import mock
16+
import argparse # Ensure argparse is imported
17+
18+
from google.ads.googleads.errors import GoogleAdsException
19+
from .test_utils import create_mock_google_ads_exception
20+
21+
# Assuming the script to be tested is in the parent directory.
22+
# Adjust the import path as necessary if the script is located elsewhere.
23+
from examples.misc import add_ad_group_image_asset
24+
25+
26+
class TestAddAdGroupImageAsset(unittest.TestCase):
27+
"""Tests for the add_ad_group_image_asset script."""
28+
29+
@mock.patch("examples.misc.add_ad_group_image_asset.GoogleAdsClient")
30+
def setUp(self, mock_google_ads_client_class): # Renamed for clarity
31+
"""Set up mock objects for testing."""
32+
self.mock_client = mock_google_ads_client_class.load_from_storage.return_value
33+
self.mock_ad_group_asset_service = self.mock_client.get_service(
34+
"AdGroupAssetService"
35+
)
36+
# Mock the path helper methods on the service
37+
self.mock_ad_group_asset_service.ad_group_path = mock.Mock(
38+
side_effect=lambda cust_id, ag_id: f"customers/{cust_id}/adGroups/{ag_id}"
39+
)
40+
self.mock_ad_group_asset_service.asset_path = mock.Mock(
41+
side_effect=lambda cust_id, asset_id_val: f"customers/{cust_id}/assets/{asset_id_val}"
42+
)
43+
self.mock_ad_group_asset_operation = self.mock_client.get_type(
44+
"AdGroupAssetOperation"
45+
)
46+
# Mock the constructor for AdGroupAsset
47+
self.mock_ad_group_asset = self.mock_client.get_type("AdGroupAsset")
48+
49+
# Mock the response from mutate_ad_group_assets
50+
self.mock_mutate_response = mock.Mock()
51+
# Create a mock result object that would be in the results list
52+
mock_result = mock.Mock()
53+
mock_result.resource_name = "test_resource_name"
54+
self.mock_mutate_response.results = [mock_result] # Make .results an iterable
55+
self.mock_ad_group_asset_service.mutate_ad_group_assets.return_value = (
56+
self.mock_mutate_response
57+
)
58+
59+
def test_main_success(self):
60+
"""Test a successful run of the main function."""
61+
customer_id = "1234567890"
62+
ad_group_id = "9876543210"
63+
image_asset_id = "1122334455"
64+
65+
add_ad_group_image_asset.main(
66+
self.mock_client, customer_id, ad_group_id, image_asset_id
67+
)
68+
69+
self.mock_ad_group_asset_service.mutate_ad_group_assets.assert_called_once()
70+
# Get the call arguments to inspect them
71+
call_args = self.mock_ad_group_asset_service.mutate_ad_group_assets.call_args
72+
# Expected operation
73+
expected_operation = self.mock_ad_group_asset_operation.return_value
74+
# Check that the customer_id in the call matches
75+
self.assertEqual(call_args[1]["customer_id"], customer_id)
76+
# Check that the operation passed to the service matches expectations
77+
# This requires checking the attributes of the operation object
78+
# that was passed to mutate_ad_group_assets
79+
actual_operation = call_args[1]["operations"][0]
80+
self.assertEqual(actual_operation.create.ad_group, f"customers/{customer_id}/adGroups/{ad_group_id}")
81+
self.assertEqual(actual_operation.create.asset, f"customers/{customer_id}/assets/{image_asset_id}")
82+
83+
84+
def test_main_google_ads_exception(self):
85+
"""Test handling of GoogleAdsException."""
86+
customer_id = "1234567890"
87+
ad_group_id = "9876543210"
88+
image_asset_id = "1122334455"
89+
90+
# Configure the mock service to raise GoogleAdsException
91+
# Mock objects needed for GoogleAdsException instantiation
92+
mock_error = mock.Mock()
93+
# It's common for error details to be complex; mocking specific attributes
94+
# that the code under test might access.
95+
# For example, if the code accesses ex.failure.errors[0].error_code.name
96+
mock_error_detail = mock.Mock()
97+
mock_error_detail.error_code.name = "TEST_ERROR" # Example error code name
98+
mock_error_detail.message = "Test failure message"
99+
# If the error object has a location, mock that too
100+
mock_error_detail.location.field_path_elements = []
101+
102+
103+
mock_failure = self.mock_client.get_type("GoogleAdsFailure")
104+
mock_failure.errors = [mock_error_detail] # Assign the detailed mock error
105+
106+
mock_call = mock.Mock()
107+
mock_request_id = "test_request_id"
108+
109+
self.mock_ad_group_asset_service.mutate_ad_group_assets.side_effect = (
110+
GoogleAdsException(mock_error, mock_call, mock_request_id, mock_failure)
111+
)
112+
113+
with self.assertRaises(GoogleAdsException):
114+
add_ad_group_image_asset.main(
115+
self.mock_client, customer_id, ad_group_id, image_asset_id
116+
)
117+
118+
def _simulate_script_main_block(
119+
self,
120+
mock_argparse_class_from_decorator,
121+
mock_gads_client_class_from_decorator,
122+
mock_main_func_from_decorator,
123+
# Expected script arguments for this test run
124+
expected_customer_id,
125+
expected_ad_group_id,
126+
expected_asset_id
127+
):
128+
# This function simulates the script's if __name__ == "__main__": block logic.
129+
130+
# 1. Configure ArgumentParser mock
131+
mock_parser_instance = mock.Mock(name="ArgumentParserInstance")
132+
mock_argparse_class_from_decorator.return_value = mock_parser_instance
133+
134+
mock_parsed_args_obj = argparse.Namespace(
135+
customer_id=expected_customer_id,
136+
ad_group_id=expected_ad_group_id,
137+
asset_id=expected_asset_id # Corrected attribute name
138+
)
139+
mock_parser_instance.parse_args.return_value = mock_parsed_args_obj
140+
141+
# Script's ArgumentParser instantiation
142+
script_description = "Updates an ad group for specified customer and ad group id with the given image asset id."
143+
parser = mock_argparse_class_from_decorator(description=script_description)
144+
145+
# Script's add_argument calls
146+
parser.add_argument(
147+
"-c", "--customer_id", type=str, required=True, help="The Google Ads customer ID."
148+
)
149+
parser.add_argument(
150+
"-a", "--ad_group_id", type=str, required=True, help="The ad group ID."
151+
)
152+
parser.add_argument(
153+
"-s", "--asset_id", type=str, required=True, help="The asset ID." # Script uses -s
154+
)
155+
156+
# Script's parse_args call
157+
args = parser.parse_args()
158+
159+
# Script's GoogleAdsClient.load_from_storage call
160+
mock_client_instance = mock.Mock(name="GoogleAdsClientInstance")
161+
mock_gads_client_class_from_decorator.load_from_storage.return_value = mock_client_instance
162+
googleads_client = mock_gads_client_class_from_decorator.load_from_storage(version="v19")
163+
164+
# Script's main function call
165+
mock_main_func_from_decorator(
166+
googleads_client,
167+
args.customer_id,
168+
args.ad_group_id,
169+
args.asset_id # Corrected attribute name
170+
)
171+
172+
@mock.patch("sys.exit")
173+
@mock.patch("examples.misc.add_ad_group_image_asset.argparse.ArgumentParser")
174+
@mock.patch("examples.misc.add_ad_group_image_asset.GoogleAdsClient")
175+
@mock.patch("examples.misc.add_ad_group_image_asset.main")
176+
def test_argument_parsing(
177+
self, mock_script_main, mock_gads_client_class,
178+
mock_arg_parser_class, mock_sys_exit
179+
):
180+
"""Test that main is called with parsed arguments."""
181+
expected_cust_id = "test_cust_123"
182+
expected_ag_id = "test_ag_456"
183+
expected_asset_id_val = "test_asset_789"
184+
185+
self._simulate_script_main_block(
186+
mock_argparse_class_from_decorator=mock_arg_parser_class,
187+
mock_gads_client_class_from_decorator=mock_gads_client_class,
188+
mock_main_func_from_decorator=mock_script_main,
189+
expected_customer_id=expected_cust_id,
190+
expected_ad_group_id=expected_ag_id,
191+
expected_asset_id=expected_asset_id_val
192+
)
193+
194+
# Assertions
195+
script_description = "Updates an ad group for specified customer and ad group id with the given image asset id."
196+
mock_arg_parser_class.assert_called_once_with(description=script_description)
197+
198+
mock_parser_instance_for_assert = mock_arg_parser_class.return_value
199+
200+
expected_calls_to_add_argument = [
201+
mock.call("-c", "--customer_id", type=str, required=True, help="The Google Ads customer ID."),
202+
mock.call("-a", "--ad_group_id", type=str, required=True, help="The ad group ID."),
203+
mock.call("-s", "--asset_id", type=str, required=True, help="The asset ID.")
204+
]
205+
mock_parser_instance_for_assert.add_argument.assert_has_calls(expected_calls_to_add_argument, any_order=True)
206+
self.assertEqual(mock_parser_instance_for_assert.add_argument.call_count, len(expected_calls_to_add_argument))
207+
208+
mock_parser_instance_for_assert.parse_args.assert_called_once_with()
209+
210+
mock_gads_client_class.load_from_storage.assert_called_once_with(version="v19")
211+
212+
client_instance_for_assert = mock_gads_client_class.load_from_storage.return_value
213+
mock_script_main.assert_called_once_with(
214+
client_instance_for_assert,
215+
expected_cust_id,
216+
expected_ag_id,
217+
expected_asset_id_val
218+
)

0 commit comments

Comments
 (0)