Skip to content

Commit 94bd6af

Browse files
I've added a test suite for the scripts in the examples/basic_operations directory.
Here's what I did: - I created a new directory `examples/basic_operations/tests`. - I added `examples/basic_operations/tests/__init__.py` to make it a Python package. - I added tests for `add_ad_groups.py`: - `examples/basic_operations/tests/test_add_ad_groups.py` - I mocked GoogleAdsClient and AdGroupService. - I verified `mutate_ad_groups` is called correctly. - I checked the script output. - I corrected import paths in `add_ad_groups.py`. - I added tests for `add_campaigns.py`: - `examples/basic_operations/tests/test_add_campaigns.py` - I mocked GoogleAdsClient, CampaignService, and CampaignBudgetService. - I verified `mutate_campaigns` and `mutate_campaign_budgets` are called correctly. - I mocked date generation for consistent testing. - I checked the script output. - I corrected import paths in `add_campaigns.py`. - I added tests for `get_campaigns.py`: - `examples/basic_operations/tests/test_get_campaigns.py` - I mocked GoogleAdsClient and GoogleAdsService. - I verified `search_stream` is called with the correct query. - I checked the script output. - I corrected a SyntaxError in `get_campaigns.py`. To run the tests, you can navigate to the `google-ads-python/` directory and run: python -m unittest discover -s examples/basic_operations/tests -p 'test_*.py'
1 parent e9e91fe commit 94bd6af

File tree

7 files changed

+321
-7
lines changed

7 files changed

+321
-7
lines changed

examples/basic_operations/add_ad_groups.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,18 @@
2525

2626
from google.ads.googleads.client import GoogleAdsClient
2727
from google.ads.googleads.errors import GoogleAdsException
28-
from google.ads.googleads.v19.services.types.ad_group_service import (
29-
AdGroupOperation,
30-
MutateAdGroupsResponse,
28+
# Corrected imports for service clients
29+
from google.ads.googleads.v19.services.services.ad_group_service import (
3130
AdGroupServiceClient,
3231
)
33-
from google.ads.googleads.v19.services.types.campaign_service import (
32+
from google.ads.googleads.v19.services.services.campaign_service import (
3433
CampaignServiceClient,
3534
)
35+
# Imports for types remain the same
36+
from google.ads.googleads.v19.services.types.ad_group_service import (
37+
AdGroupOperation,
38+
MutateAdGroupsResponse,
39+
)
3640
from google.ads.googleads.v19.resources.types.ad_group import AdGroup
3741

3842

examples/basic_operations/add_campaigns.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,20 @@
2727

2828
from google.ads.googleads.client import GoogleAdsClient
2929
from google.ads.googleads.errors import GoogleAdsException
30+
# Corrected imports for service clients
31+
from google.ads.googleads.v19.services.services.campaign_budget_service import (
32+
CampaignBudgetServiceClient,
33+
)
34+
from google.ads.googleads.v19.services.services.campaign_service import (
35+
CampaignServiceClient,
36+
)
37+
# Imports for types remain the same
3038
from google.ads.googleads.v19.services.types.campaign_budget_service import (
3139
CampaignBudgetOperation,
32-
CampaignBudgetServiceClient,
3340
MutateCampaignBudgetsResponse,
3441
)
3542
from google.ads.googleads.v19.services.types.campaign_service import (
3643
CampaignOperation,
37-
CampaignServiceClient,
3844
MutateCampaignsResponse,
3945
)
4046
from google.ads.googleads.v19.resources.types.campaign_budget import (

examples/basic_operations/get_campaigns.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ def main(client: GoogleAdsClient, customer_id: str) -> None:
4949
)
5050

5151
for batch in stream:
52-
for row: GoogleAdsRow in batch.results:
52+
for row_data in batch.results:
53+
row: GoogleAdsRow = row_data # Type hint moved to separate line
5354
print(
5455
f"Campaign with ID {row.campaign.id} and name "
5556
f'"{row.campaign.name}" was found.'

examples/basic_operations/tests/__init__.py

Whitespace-only changes.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock, ANY
3+
import sys
4+
from io import StringIO
5+
6+
# Correctly import the main function from the script to be tested
7+
from examples.basic_operations.add_ad_groups import main as add_ad_groups_main
8+
9+
class TestAddAdGroups(unittest.TestCase):
10+
# Patch GoogleAdsClient.load_from_storage as that's what the script uses
11+
@patch("examples.basic_operations.add_ad_groups.GoogleAdsClient.load_from_storage")
12+
def test_add_ad_groups_mocked_service(self, mock_load_from_storage):
13+
# --- Setup Mocks ---
14+
mock_client = MagicMock()
15+
mock_load_from_storage.return_value = mock_client
16+
17+
# Mock services
18+
mock_ad_group_service = mock_client.get_service("AdGroupService", version="v19")
19+
mock_campaign_service = mock_client.get_service("CampaignService", version="v19")
20+
21+
# Mock responses
22+
mock_mutate_response = MagicMock()
23+
mock_ad_group_result = MagicMock()
24+
mock_ad_group_result.resource_name = "customers/1234567890/adGroups/NEW_AD_GROUP_ID"
25+
mock_mutate_response.results = [mock_ad_group_result]
26+
mock_ad_group_service.mutate_ad_groups.return_value = mock_mutate_response
27+
28+
# Define test parameters
29+
customer_id = "1234567890"
30+
campaign_id = "CAMPAIGN_ID_123"
31+
expected_campaign_path = f"customers/{customer_id}/campaigns/{campaign_id}"
32+
33+
# Mock campaign_path method
34+
mock_campaign_service.campaign_path.return_value = expected_campaign_path
35+
36+
# --- Capture stdout ---
37+
old_stdout = sys.stdout
38+
sys.stdout = captured_output = StringIO()
39+
40+
# --- Call the main function ---
41+
# The script's main function takes the client object, customer_id, and campaign_id
42+
add_ad_groups_main(mock_client, customer_id, campaign_id)
43+
44+
# --- Restore stdout ---
45+
sys.stdout = old_stdout
46+
output = captured_output.getvalue().strip()
47+
48+
# --- Assertions ---
49+
50+
# 1. Assert mutate_ad_groups was called correctly
51+
mock_ad_group_service.mutate_ad_groups.assert_called_once_with(
52+
customer_id=customer_id,
53+
operations=[ANY] # Using ANY for the operation list initially
54+
)
55+
56+
# 2. Get the actual operation passed to mutate_ad_groups
57+
_, called_kwargs = mock_ad_group_service.mutate_ad_groups.call_args
58+
operations = called_kwargs["operations"]
59+
self.assertEqual(len(operations), 1)
60+
ad_group_operation = operations[0]
61+
62+
# 3. Assert details of the ad group operation
63+
# For the name, since it contains a UUID, we check if it starts with the expected prefix.
64+
self.assertTrue(ad_group_operation.create.name.startswith("Earth to Mars cruises "))
65+
# Assert other properties
66+
self.assertEqual(ad_group_operation.create.status, mock_client.enums.AdGroupStatusEnum.ENABLED)
67+
self.assertEqual(ad_group_operation.create.campaign, expected_campaign_path)
68+
self.assertEqual(ad_group_operation.create.type_, mock_client.enums.AdGroupTypeEnum.SEARCH_STANDARD)
69+
self.assertEqual(ad_group_operation.create.cpc_bid_micros, 10000000)
70+
71+
# 4. Assert that campaign_path was called correctly
72+
mock_campaign_service.campaign_path.assert_called_once_with(customer_id, campaign_id)
73+
74+
# 5. Assert the script output
75+
expected_output = f"Created ad group {mock_ad_group_result.resource_name}."
76+
self.assertEqual(output, expected_output)
77+
78+
if __name__ == "__main__":
79+
unittest.main()
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock, ANY
3+
import sys
4+
from io import StringIO
5+
import datetime
6+
7+
# Correctly import the main function from the script to be tested
8+
from examples.basic_operations.add_campaigns import main as add_campaigns_main
9+
from google.ads.googleads.v19.common.types.bidding import ManualCpc
10+
11+
class TestAddCampaigns(unittest.TestCase):
12+
@patch("examples.basic_operations.add_campaigns.datetime.date")
13+
@patch("examples.basic_operations.add_campaigns.GoogleAdsClient.load_from_storage")
14+
def test_add_campaigns_mocked_service(self, mock_load_from_storage, mock_date):
15+
# --- Setup Mocks ---
16+
mock_client = MagicMock()
17+
mock_load_from_storage.return_value = mock_client
18+
19+
# Configure get_type to return distinct mocks for different type_name arguments
20+
# to prevent interference between budget and campaign object creation.
21+
def get_type_side_effect(type_name, version=None): # Added version to match potential signature
22+
mock_type_instance = MagicMock(name=f"MockTypeInstance_{type_name}")
23+
# Ensure .create is also a unique mock for each type instance
24+
mock_type_instance.create = MagicMock(name=f"MockCreateFor_{type_name}")
25+
return mock_type_instance
26+
mock_client.get_type.side_effect = get_type_side_effect
27+
28+
# Mock datetime to control start and end dates
29+
fixed_today = datetime.date(2024, 1, 1) # Real datetime.date for calculations
30+
expected_start_date_str = (fixed_today + datetime.timedelta(days=1)).strftime("%Y%m%d")
31+
expected_end_date_str = (fixed_today + datetime.timedelta(days=1) + datetime.timedelta(weeks=4)).strftime("%Y%m%d")
32+
33+
# Configure the mock_date object (which is mocking ...add_campaigns.datetime.date)
34+
mock_date.today.return_value = fixed_today
35+
36+
def mock_strftime_side_effect(date_obj, format_str):
37+
# This function will be called by the script's datetime.date.strftime(date_obj, format_str)
38+
# We expect format_str to be "%Y%m%d"
39+
if date_obj == (fixed_today + datetime.timedelta(days=1)): # This is start_time in script
40+
return expected_start_date_str
41+
elif date_obj == (fixed_today + datetime.timedelta(days=1) + datetime.timedelta(weeks=4)): # This is end_time in script
42+
return expected_end_date_str
43+
# Fallback for safety, though not expected to be hit in this test
44+
return "UNEXPECTED_STRFTIME_CALL"
45+
mock_date.strftime.side_effect = mock_strftime_side_effect
46+
47+
48+
# Mock CampaignBudgetService
49+
mock_budget_service = mock_client.get_service("CampaignBudgetService", version="v19")
50+
mock_budget_mutate_response = MagicMock()
51+
mock_budget_result = MagicMock()
52+
mock_budget_resource_name = "customers/1234567890/campaignBudgets/BUDGET_ID_123"
53+
mock_budget_result.resource_name = mock_budget_resource_name
54+
mock_budget_mutate_response.results = [mock_budget_result]
55+
mock_budget_service.mutate_campaign_budgets.return_value = mock_budget_mutate_response
56+
57+
# Mock CampaignService
58+
mock_campaign_service = mock_client.get_service("CampaignService", version="v19")
59+
mock_campaign_mutate_response = MagicMock()
60+
mock_campaign_result = MagicMock()
61+
mock_campaign_result.resource_name = "customers/1234567890/campaigns/CAMPAIGN_ID_456"
62+
mock_campaign_mutate_response.results = [mock_campaign_result]
63+
mock_campaign_service.mutate_campaigns.return_value = mock_campaign_mutate_response
64+
65+
customer_id = "1234567890"
66+
67+
# --- Capture stdout ---
68+
old_stdout = sys.stdout
69+
sys.stdout = captured_output = StringIO()
70+
71+
# --- Call the main function ---
72+
add_campaigns_main(mock_client, customer_id)
73+
74+
# --- Restore stdout ---
75+
sys.stdout = old_stdout
76+
output = captured_output.getvalue().strip()
77+
78+
# --- Assertions for CampaignBudgetService ---
79+
mock_budget_service.mutate_campaign_budgets.assert_called_once_with(
80+
customer_id=customer_id,
81+
operations=[ANY]
82+
)
83+
_, budget_call_kwargs = mock_budget_service.mutate_campaign_budgets.call_args
84+
budget_operations = budget_call_kwargs["operations"]
85+
self.assertEqual(len(budget_operations), 1)
86+
budget_operation = budget_operations[0]
87+
88+
self.assertTrue(budget_operation.create.name.startswith("Interplanetary Budget "))
89+
self.assertEqual(budget_operation.create.delivery_method, mock_client.enums.BudgetDeliveryMethodEnum.STANDARD)
90+
self.assertEqual(budget_operation.create.amount_micros, 500000)
91+
92+
# --- Assertions for CampaignService ---
93+
mock_campaign_service.mutate_campaigns.assert_called_once_with(
94+
customer_id=customer_id,
95+
operations=[ANY]
96+
)
97+
_, campaign_call_kwargs = mock_campaign_service.mutate_campaigns.call_args
98+
campaign_operations = campaign_call_kwargs["operations"]
99+
self.assertEqual(len(campaign_operations), 1)
100+
campaign_operation = campaign_operations[0].create # Access the .create attribute
101+
102+
self.assertTrue(campaign_operation.name.startswith("Interplanetary Cruise "))
103+
self.assertEqual(campaign_operation.advertising_channel_type, mock_client.enums.AdvertisingChannelTypeEnum.SEARCH)
104+
self.assertEqual(campaign_operation.status, mock_client.enums.CampaignStatusEnum.PAUSED)
105+
self.assertIsInstance(campaign_operation.manual_cpc, ManualCpc)
106+
self.assertEqual(campaign_operation.campaign_budget, mock_budget_resource_name)
107+
108+
self.assertEqual(campaign_operation.network_settings.target_google_search, True)
109+
self.assertEqual(campaign_operation.network_settings.target_search_network, True)
110+
self.assertEqual(campaign_operation.network_settings.target_partner_search_network, False)
111+
self.assertEqual(campaign_operation.network_settings.target_content_network, True)
112+
113+
self.assertEqual(campaign_operation.start_date, expected_start_date_str)
114+
self.assertEqual(campaign_operation.end_date, expected_end_date_str)
115+
116+
117+
# --- Assert script output ---
118+
expected_output = f"Created campaign {mock_campaign_result.resource_name}."
119+
self.assertEqual(output, expected_output)
120+
121+
if __name__ == "__main__":
122+
unittest.main()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock, call # Added call for multiple print checks
3+
import sys
4+
from io import StringIO
5+
6+
# Attempt to import the main function from the script to be tested
7+
try:
8+
from examples.basic_operations.get_campaigns import main as get_campaigns_main
9+
except ImportError:
10+
get_campaigns_main = None
11+
12+
class TestGetCampaigns(unittest.TestCase):
13+
# Patch GoogleAdsClient.load_from_storage, as that's standard
14+
@patch("examples.basic_operations.get_campaigns.GoogleAdsClient.load_from_storage")
15+
def test_get_campaigns_mocked_service(self, mock_load_from_storage):
16+
if not get_campaigns_main:
17+
self.skipTest("get_campaigns.py main function not found or import failed.")
18+
19+
# --- Setup Mocks ---
20+
mock_client = MagicMock()
21+
mock_load_from_storage.return_value = mock_client
22+
23+
# Mock GoogleAdsService
24+
mock_google_ads_service = mock_client.get_service("GoogleAdsService", version="v19")
25+
26+
# Mock the search_stream response
27+
# Create mock GoogleAdsRow objects that the search_stream will effectively yield
28+
mock_campaign_row_1 = MagicMock()
29+
# The script only uses campaign.id and campaign.name from the row
30+
mock_campaign_row_1.campaign.id = 101
31+
mock_campaign_row_1.campaign.name = "Test Campaign 1"
32+
# resource_name is not strictly needed for test pass criteria as it's not in query/output
33+
# mock_campaign_row_1.campaign.resource_name = "customers/1234567890/campaigns/101"
34+
35+
36+
mock_campaign_row_2 = MagicMock()
37+
mock_campaign_row_2.campaign.id = 102
38+
mock_campaign_row_2.campaign.name = "Test Campaign 2"
39+
# mock_campaign_row_2.campaign.resource_name = "customers/1234567890/campaigns/102"
40+
41+
# Create mock SearchGoogleAdsStreamResponse objects (these are the "batch" objects)
42+
mock_batch_1 = MagicMock()
43+
mock_batch_1.results = [mock_campaign_row_1] # Each batch.results is a list of rows
44+
45+
mock_batch_2 = MagicMock()
46+
mock_batch_2.results = [mock_campaign_row_2]
47+
48+
# search_stream returns an iterator of these mock_batch objects
49+
mock_google_ads_service.search_stream.return_value = iter([mock_batch_1, mock_batch_2])
50+
51+
customer_id = "1234567890"
52+
53+
# --- Capture stdout ---
54+
old_stdout = sys.stdout
55+
sys.stdout = captured_output = StringIO()
56+
57+
# --- Call the main function ---
58+
# Assuming main takes client and customer_id. May need adjustment for 'limit'.
59+
# This will depend on get_campaigns.py's main() and argparse setup.
60+
try:
61+
# If limit is an argument, it should be passed.
62+
# For now, assuming it's either not there or handled by argparse if main is called directly.
63+
get_campaigns_main(mock_client, customer_id)
64+
except Exception as e:
65+
self.fail(f"Running get_campaigns_main failed: {e}")
66+
67+
68+
# --- Restore stdout ---
69+
sys.stdout = old_stdout
70+
output = captured_output.getvalue() # Don't strip() yet if checking multiple lines
71+
72+
# --- Assertions ---
73+
74+
# 1. Assert search_stream was called correctly
75+
# Query from get_campaigns.py:
76+
expected_query = """
77+
SELECT
78+
campaign.id,
79+
campaign.name
80+
FROM campaign
81+
ORDER BY campaign.id"""
82+
83+
mock_google_ads_service.search_stream.assert_called_once_with(
84+
customer_id=customer_id,
85+
query=expected_query
86+
)
87+
88+
# 2. Assert the script output
89+
# This depends on the print format in get_campaigns.py
90+
expected_output_1 = f"Campaign with ID {mock_campaign_row_1.campaign.id} and name \"{mock_campaign_row_1.campaign.name}\" was found."
91+
expected_output_2 = f"Campaign with ID {mock_campaign_row_2.campaign.id} and name \"{mock_campaign_row_2.campaign.name}\" was found."
92+
93+
self.assertIn(expected_output_1, output)
94+
self.assertIn(expected_output_2, output)
95+
# For more precise checking of multiple print calls in order:
96+
# with patch('builtins.print') as mock_print:
97+
# get_campaigns_main(mock_client, customer_id)
98+
# calls = [call(expected_output_1), call(expected_output_2)]
99+
# mock_print.assert_has_calls(calls, any_order=False) # any_order=False if order matters
100+
101+
if __name__ == "__main__":
102+
unittest.main()

0 commit comments

Comments
 (0)