Skip to content

Commit 4e18590

Browse files
feat: Add test suite for examples/misc
This commit introduces a new test suite for the Python scripts located in the examples/misc directory. The following scripts now have corresponding unit tests: - add_ad_group_image_asset.py - campaign_report_to_csv.py - set_custom_client_timeouts.py - upload_image_asset.py The tests are located in the examples/misc/tests subdirectory. They utilize unittest.mock to mock Google Ads API interactions, ensuring that the scripts' logic is verified without making actual API calls. Key aspects tested include: - Correct service calls and parameter construction. - Proper handling of API responses and exceptions (e.g., DeadlineExceeded). - File I/O operations (e.g., CSV generation). - Correct usage of client configurations like timeouts and retries. An __init__.py file has been added to the tests directory to enable test discovery.
1 parent 7ffe09c commit 4e18590

File tree

6 files changed

+474
-0
lines changed

6 files changed

+474
-0
lines changed

examples/misc/tests/.gitkeep

Whitespace-only changes.

examples/misc/tests/__init__.py

Whitespace-only changes.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock
3+
import sys
4+
import os
5+
6+
# Add the project root directory to sys.path to allow robust import of the script under test.
7+
# This assumes the test file is located at 'examples/misc/tests/test_add_ad_group_image_asset.py'
8+
# and the script to test is at 'examples/misc/add_ad_group_image_asset.py'.
9+
# The project root would then be '../..' from this file's directory.
10+
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
11+
if _PROJECT_ROOT not in sys.path:
12+
sys.path.insert(0, _PROJECT_ROOT)
13+
14+
from examples.misc import add_ad_group_image_asset
15+
16+
class TestAddAdGroupImageAsset(unittest.TestCase):
17+
18+
@patch('examples.misc.add_ad_group_image_asset.GoogleAdsClient.load_from_storage')
19+
def test_main_function(self, mock_load_from_storage):
20+
# Mock the GoogleAdsClient instance
21+
mock_ads_client = MagicMock()
22+
mock_load_from_storage.return_value = mock_ads_client
23+
24+
# Mock the AdGroupAssetService
25+
mock_ad_group_asset_service = mock_ads_client.get_service.return_value
26+
27+
# Define test parameters and expected values
28+
customer_id = "1234567890"
29+
ad_group_id = "111222333"
30+
asset_id = "987654321"
31+
32+
expected_asset_resource_name = f"customers/{customer_id}/assets/{asset_id}"
33+
expected_ad_group_resource_name = f"customers/{customer_id}/adGroups/{ad_group_id}"
34+
mock_created_resource_name = f"customers/{customer_id}/adGroupAssets/{ad_group_id}~{asset_id}"
35+
36+
# Configure mock path helper methods
37+
mock_ad_group_asset_service.asset_path.return_value = expected_asset_resource_name
38+
mock_ad_group_asset_service.ad_group_path.return_value = expected_ad_group_resource_name
39+
40+
# Configure mock for client.get_type("AdGroupAssetOperation")
41+
# This mock will be used by the script to create the operation object
42+
mock_operation = MagicMock()
43+
mock_ads_client.get_type.return_value = mock_operation
44+
45+
# Configure mock for AssetFieldTypeEnum
46+
# The script accesses client.enums.AssetFieldTypeEnum.AD_IMAGE
47+
mock_ads_client.enums.AssetFieldTypeEnum.AD_IMAGE = "AD_IMAGE_ENUM_VALUE_FOR_TEST"
48+
49+
# Configure mock for the mutate_ad_group_assets method response
50+
mock_mutate_response = MagicMock()
51+
mock_mutate_response.results = [MagicMock(resource_name=mock_created_resource_name)]
52+
mock_ad_group_asset_service.mutate_ad_group_assets.return_value = mock_mutate_response
53+
54+
# Call the main function of the script, capturing print output
55+
with patch('builtins.print') as mock_print:
56+
add_ad_group_image_asset.main(
57+
mock_ads_client,
58+
customer_id,
59+
ad_group_id,
60+
asset_id
61+
)
62+
63+
# --- Assertions ---
64+
65+
# Verify GoogleAdsClient.load_from_storage was called (implicitly by @patch)
66+
mock_load_from_storage.assert_called_once() # Path is checked by the patch decorator
67+
68+
# Verify AdGroupAssetService was fetched
69+
mock_ads_client.get_service.assert_called_once_with("AdGroupAssetService")
70+
71+
# Verify path helper methods were called with correct parameters
72+
mock_ad_group_asset_service.asset_path.assert_called_once_with(customer_id, asset_id)
73+
mock_ad_group_asset_service.ad_group_path.assert_called_once_with(customer_id, ad_group_id)
74+
75+
# Verify client.get_type("AdGroupAssetOperation") was called
76+
mock_ads_client.get_type.assert_called_once_with("AdGroupAssetOperation")
77+
78+
# Verify the mutate_ad_group_assets call
79+
# The script constructs an operation and passes it.
80+
# We need to check the arguments of this call.
81+
82+
# Expected operation built by the script:
83+
# ad_group_asset_operation = client.get_type("AdGroupAssetOperation") (this is our mock_operation)
84+
# ad_group_asset_set = ad_group_asset_operation.create
85+
# ad_group_asset_set.asset = expected_asset_resource_name
86+
# ad_group_asset_set.field_type = client.enums.AssetFieldTypeEnum.AD_IMAGE
87+
# ad_group_asset_set.ad_group = expected_ad_group_resource_name
88+
89+
# Check that the 'create' attribute of our mock_operation was configured as expected
90+
self.assertEqual(mock_operation.create.asset, expected_asset_resource_name)
91+
self.assertEqual(mock_operation.create.ad_group, expected_ad_group_resource_name)
92+
self.assertEqual(mock_operation.create.field_type, mock_ads_client.enums.AssetFieldTypeEnum.AD_IMAGE)
93+
94+
# Now, check if mutate_ad_group_assets was called with this 'mock_operation'
95+
mock_ad_group_asset_service.mutate_ad_group_assets.assert_called_once_with(
96+
customer_id=customer_id,
97+
operations=[mock_operation] # The script passes the operation it built
98+
)
99+
100+
# Verify the printed output
101+
expected_print_message = (
102+
f"Created ad group asset with resource name: '{mock_created_resource_name}'"
103+
)
104+
# The script loops through results, so check if any call matches.
105+
mock_print.assert_any_call(expected_print_message)
106+
107+
if __name__ == "__main__":
108+
unittest.main()
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock, call
3+
import csv
4+
import os
5+
import sys
6+
from datetime import datetime
7+
8+
# Adjust sys.path to allow import of the script under test
9+
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
10+
if _PROJECT_ROOT not in sys.path:
11+
sys.path.insert(0, _PROJECT_ROOT)
12+
13+
from examples.misc import campaign_report_to_csv
14+
15+
# Define a fixed output directory for tests to manage created files
16+
# Assuming os.path.join behavior: os.path.join("anything", "/absolute/path") returns "/absolute/path"
17+
# If _TEST_CSV_FILEPATH is absolute, the script should use it directly.
18+
_TEST_OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "test_output")
19+
_TEST_CSV_FILENAME = "test_campaign_report.csv"
20+
_TEST_CSV_FILEPATH = os.path.abspath(os.path.join(_TEST_OUTPUT_DIR, _TEST_CSV_FILENAME))
21+
22+
23+
class TestCampaignReportToCsv(unittest.TestCase):
24+
25+
def setUp(self):
26+
if not os.path.exists(_TEST_OUTPUT_DIR):
27+
os.makedirs(_TEST_OUTPUT_DIR)
28+
if os.path.exists(_TEST_CSV_FILEPATH):
29+
os.remove(_TEST_CSV_FILEPATH)
30+
31+
def tearDown(self):
32+
if os.path.exists(_TEST_CSV_FILEPATH):
33+
os.remove(_TEST_CSV_FILEPATH)
34+
if os.path.exists(_TEST_OUTPUT_DIR) and not os.listdir(_TEST_OUTPUT_DIR):
35+
os.rmdir(_TEST_OUTPUT_DIR)
36+
37+
def _create_mock_google_ads_row(self, descriptive_name, date, campaign_name, impressions, clicks, cost_micros):
38+
"""Helper to create a MagicMock object that mimics a GoogleAdsRow based on script's output."""
39+
row = MagicMock()
40+
row.customer.descriptive_name = descriptive_name
41+
row.segments.date = date
42+
row.campaign.name = campaign_name
43+
row.metrics.impressions = impressions
44+
row.metrics.clicks = clicks
45+
row.metrics.cost_micros = cost_micros
46+
return row
47+
48+
@patch('examples.misc.campaign_report_to_csv.GoogleAdsClient.load_from_storage')
49+
def test_report_generation_with_headers(self, mock_load_from_storage):
50+
mock_ads_client = MagicMock()
51+
mock_load_from_storage.return_value = mock_ads_client
52+
mock_ga_service = mock_ads_client.get_service.return_value
53+
54+
# Sample data matching script's output fields
55+
mock_rows_data = [
56+
("Test Account 1", "2023-10-26", "Campaign Alpha", 1000, 150, 5000000),
57+
("Test Account 1", "2023-10-26", "Campaign Beta", 2500, 300, 12000000),
58+
]
59+
# search_stream returns an iterable of batches, each batch has 'results'
60+
mock_search_stream_response = [MagicMock(results=[self._create_mock_google_ads_row(*data) for data in mock_rows_data])]
61+
mock_ga_service.search_stream.return_value = mock_search_stream_response
62+
63+
customer_id = "1234567890"
64+
65+
campaign_report_to_csv.main(
66+
mock_ads_client, customer_id, output_file=_TEST_CSV_FILEPATH, write_headers=True
67+
)
68+
69+
self.assertTrue(os.path.exists(_TEST_CSV_FILEPATH))
70+
71+
# Script's actual headers
72+
expected_headers = ["Account", "Date", "Campaign", "Impressions", "Clicks", "Cost"]
73+
with open(_TEST_CSV_FILEPATH, mode='r', newline='') as csvfile:
74+
reader = csv.reader(csvfile)
75+
headers = next(reader)
76+
self.assertEqual(headers, expected_headers)
77+
78+
content = list(reader)
79+
self.assertEqual(len(content), len(mock_rows_data))
80+
for i, row_data in enumerate(mock_rows_data):
81+
self.assertEqual(content[i][0], str(row_data[0])) # Account
82+
self.assertEqual(content[i][1], str(row_data[1])) # Date
83+
self.assertEqual(content[i][2], str(row_data[2])) # Campaign
84+
self.assertEqual(content[i][3], str(row_data[3])) # Impressions
85+
self.assertEqual(content[i][4], str(row_data[4])) # Clicks
86+
self.assertEqual(content[i][5], str(row_data[5])) # Cost
87+
88+
mock_ga_service.search_stream.assert_called_once()
89+
# Verify the search_request object passed to search_stream
90+
args, _ = mock_ga_service.search_stream.call_args
91+
search_request = args[0]
92+
self.assertEqual(search_request.customer_id, customer_id)
93+
self.assertIn("FROM campaign", search_request.query)
94+
95+
96+
@patch('examples.misc.campaign_report_to_csv.GoogleAdsClient.load_from_storage')
97+
def test_report_generation_without_headers(self, mock_load_from_storage):
98+
mock_ads_client = MagicMock()
99+
mock_load_from_storage.return_value = mock_ads_client
100+
mock_ga_service = mock_ads_client.get_service.return_value
101+
102+
mock_rows_data = [
103+
("Test Account 2", "2023-10-27", "Campaign Gamma", 500, 50, 2000000),
104+
]
105+
mock_search_stream_response = [MagicMock(results=[self._create_mock_google_ads_row(*data) for data in mock_rows_data])]
106+
mock_ga_service.search_stream.return_value = mock_search_stream_response
107+
customer_id = "9876543210"
108+
109+
campaign_report_to_csv.main(
110+
mock_ads_client, customer_id, output_file=_TEST_CSV_FILEPATH, write_headers=False
111+
)
112+
113+
self.assertTrue(os.path.exists(_TEST_CSV_FILEPATH))
114+
115+
with open(_TEST_CSV_FILEPATH, mode='r', newline='') as csvfile:
116+
reader = csv.reader(csvfile)
117+
content = list(reader)
118+
self.assertEqual(len(content), len(mock_rows_data))
119+
for i, row_data in enumerate(mock_rows_data):
120+
self.assertEqual(content[i][0], str(row_data[0])) # Account
121+
self.assertEqual(content[i][1], str(row_data[1])) # Date
122+
self.assertEqual(content[i][2], str(row_data[2])) # Campaign
123+
self.assertEqual(content[i][3], str(row_data[3])) # Impressions
124+
self.assertEqual(content[i][4], str(row_data[4])) # Clicks
125+
self.assertEqual(content[i][5], str(row_data[5])) # Cost
126+
127+
mock_ga_service.search_stream.assert_called_once()
128+
args, _ = mock_ga_service.search_stream.call_args
129+
search_request = args[0]
130+
self.assertEqual(search_request.customer_id, customer_id)
131+
132+
if __name__ == "__main__":
133+
unittest.main()
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock, ANY
3+
import sys
4+
import os
5+
6+
from google.api_core.exceptions import DeadlineExceeded
7+
from google.api_core import retry as api_core_retry
8+
9+
# Adjust sys.path to allow import of the script under test
10+
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
11+
if _PROJECT_ROOT not in sys.path:
12+
sys.path.insert(0, _PROJECT_ROOT)
13+
14+
from examples.misc import set_custom_client_timeouts
15+
16+
# _EXPECTED_PAGE_SIZE is not used in the target script.
17+
18+
class TestSetCustomClientTimeouts(unittest.TestCase):
19+
20+
def _create_mock_row_with_campaign_id(self, campaign_id="test_campaign_id_123"):
21+
row = MagicMock()
22+
row.campaign.id = campaign_id
23+
return row
24+
25+
def _create_mock_ads_client(self):
26+
mock_ads_client = MagicMock()
27+
mock_ga_service = mock_ads_client.get_service.return_value
28+
29+
# Mock for search_stream (streaming call)
30+
mock_batch = MagicMock()
31+
mock_batch.results = [self._create_mock_row_with_campaign_id()]
32+
mock_ga_service.search_stream.return_value = [mock_batch]
33+
34+
# Mock for search (unary call)
35+
mock_ga_service.search.return_value = [self._create_mock_row_with_campaign_id()]
36+
37+
return mock_ads_client, mock_ga_service
38+
39+
# --- Tests for make_server_streaming_call ---
40+
41+
@patch('set_custom_client_timeouts._CLIENT_TIMEOUT_SECONDS', 60.0) # Patch script's constant
42+
@patch('builtins.print')
43+
def test_streaming_call_success(self, mock_print):
44+
mock_ads_client, mock_ga_service = self._create_mock_ads_client()
45+
customer_id = "1234567890"
46+
47+
# Call the function without timeout arg, it uses the patched script constant
48+
set_custom_client_timeouts.make_server_streaming_call(
49+
mock_ads_client, customer_id
50+
)
51+
52+
mock_ads_client.get_service.assert_called_once_with("GoogleAdsService") # Version not specified in script func
53+
mock_ga_service.search_stream.assert_called_once_with(request=ANY, timeout=60.0) # Check patched timeout
54+
55+
# Verify print output for success
56+
mock_print.assert_any_call("The server streaming call completed before the timeout.")
57+
mock_print.assert_any_call("Total # of campaign IDs retrieved: 1")
58+
59+
60+
@patch('set_custom_client_timeouts._CLIENT_TIMEOUT_SECONDS', 0.01) # Very small timeout
61+
@patch('builtins.print')
62+
@patch('sys.exit')
63+
def test_streaming_call_deadline_exceeded(self, mock_sys_exit, mock_print):
64+
mock_ads_client, mock_ga_service = self._create_mock_ads_client()
65+
customer_id = "1234567890"
66+
67+
mock_ga_service.search_stream.side_effect = DeadlineExceeded("Test DeadlineExceeded Streaming")
68+
69+
set_custom_client_timeouts.make_server_streaming_call(
70+
mock_ads_client, customer_id
71+
)
72+
73+
mock_ga_service.search_stream.assert_called_once_with(request=ANY, timeout=0.01)
74+
mock_print.assert_any_call("The server streaming call did not complete before the timeout.")
75+
mock_sys_exit.assert_called_once_with(1)
76+
77+
# --- Tests for make_unary_call ---
78+
79+
@patch('set_custom_client_timeouts._CLIENT_TIMEOUT_SECONDS', 120.0) # Patch script's constant for deadline
80+
@patch('builtins.print')
81+
def test_unary_call_success(self, mock_print):
82+
mock_ads_client, mock_ga_service = self._create_mock_ads_client()
83+
customer_id = "1234567890"
84+
85+
# Patched _CLIENT_TIMEOUT_SECONDS will be used by the script function
86+
script_timeout = 120.0
87+
expected_initial = script_timeout / 10.0
88+
expected_maximum = script_timeout / 5.0
89+
expected_deadline = script_timeout
90+
91+
set_custom_client_timeouts.make_unary_call(
92+
mock_ads_client, customer_id
93+
)
94+
95+
mock_ads_client.get_service.assert_called_once_with("GoogleAdsService") # Version not specified in script func
96+
97+
args, kwargs = mock_ga_service.search.call_args
98+
self.assertEqual(kwargs.get('request').customer_id, customer_id)
99+
self.assertIn("SELECT campaign.id FROM campaign", kwargs.get('request').query)
100+
101+
actual_retry = kwargs.get('retry')
102+
self.assertIsNotNone(actual_retry)
103+
self.assertIsInstance(actual_retry, api_core_retry.Retry)
104+
self.assertEqual(actual_retry._initial, expected_initial)
105+
self.assertEqual(actual_retry._maximum, expected_maximum)
106+
self.assertEqual(actual_retry._deadline, expected_deadline)
107+
# Assert default multiplier and predicate if they are not set in script
108+
self.assertEqual(actual_retry._multiplier, 1.3) # google.api_core.retry.Retry._DEFAULT_MULTIPLIER
109+
self.assertTrue(callable(actual_retry._predicate)) # Check if it's a callable, like if_transient_error
110+
111+
112+
mock_print.assert_any_call("The unary call completed before the timeout.")
113+
mock_print.assert_any_call("Total # of campaign IDs retrieved: 1")
114+
115+
116+
@patch('set_custom_client_timeouts._CLIENT_TIMEOUT_SECONDS', 0.01) # Very small timeout for deadline
117+
@patch('builtins.print')
118+
@patch('sys.exit')
119+
def test_unary_call_deadline_exceeded(self, mock_sys_exit, mock_print):
120+
mock_ads_client, mock_ga_service = self._create_mock_ads_client()
121+
customer_id = "1234567890"
122+
123+
mock_ga_service.search.side_effect = DeadlineExceeded("Test DeadlineExceeded Unary")
124+
125+
set_custom_client_timeouts.make_unary_call(
126+
mock_ads_client, customer_id
127+
)
128+
129+
args, kwargs = mock_ga_service.search.call_args
130+
self.assertIsNotNone(kwargs.get('retry'))
131+
self.assertEqual(kwargs.get('retry')._deadline, 0.01)
132+
133+
134+
mock_print.assert_any_call("The unary call did not complete before the timeout.")
135+
mock_sys_exit.assert_called_once_with(1)
136+
137+
138+
if __name__ == "__main__":
139+
unittest.main()

0 commit comments

Comments
 (0)