Skip to content

Commit a9d7acc

Browse files
I've added a test suite for examples/assets using API v19.
This change introduces a comprehensive test suite for the Python scripts located in the examples/assets directory. All tests are designed to use mocked objects for version 19 of the Google Ads API. Key changes include: - Creation of a new test directory: tests/examples/assets/. - Individual test files for each script in examples/assets/: - test_add_call.py - test_add_hotel_callout.py - test_add_lead_form_asset.py - test_add_prices.py - test_add_sitelinks.py - test_upload_image_asset.py - Tests cover function logic, API call mocking (services and operations), argument parsing, and adherence to Google Ads API v19. - Updated CONTRIBUTING.md to include instructions on how to run the test suite using nox. - Test dependencies are managed via the existing nox setup.
1 parent 3b99197 commit a9d7acc

File tree

7 files changed

+1433
-0
lines changed

7 files changed

+1433
-0
lines changed

CONTRIBUTING.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,32 @@ FILES=$(git diff --cached --name-only --diff-filter=ACMR "*.py" | grep -v "googl
5454
echo "${FILES}" | xargs python -m black -l 80
5555
echo "${FILES}" | xargs git add
5656
```
57+
58+
## Running Tests
59+
60+
This project uses [nox](https://nox.thea.codes/) for automated testing and linting across different Python environments. Nox is configured in the `noxfile.py` at the root of the repository.
61+
62+
To run the test suite:
63+
64+
1. Ensure you have `nox` installed. If not, you can install it via pip:
65+
```bash
66+
python -m pip install nox
67+
```
68+
69+
2. To list all available nox sessions (which might include different Python versions, linting, etc.), run:
70+
```bash
71+
nox -l
72+
```
73+
74+
3. To run all default test sessions, execute the following command from the root of the repository:
75+
```bash
76+
nox
77+
```
78+
79+
4. If you want to run a specific test session (e.g., tests for a particular Python version or a linting session), you can specify it using the `-s` flag. For example:
80+
```bash
81+
nox -s tests-3.8
82+
```
83+
(The exact session names can be found using `nox -l`.)
84+
85+
The newly added tests for the `examples/assets` directory are part of the standard test sessions and will be executed automatically when you run `nox`.
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import pytest
2+
from unittest.mock import MagicMock, patch, call
3+
4+
from google.ads.googleads.client import GoogleAdsClient
5+
from google.ads.googleads.v19.services.types.asset_service import AssetOperation
6+
from google.ads.googleads.v19.services.types.customer_asset_service import CustomerAssetOperation
7+
from google.ads.googleads.v19.enums.types.asset_field_type import AssetFieldTypeEnum
8+
from google.ads.googleads.v19.enums.types.call_conversion_reporting_state import CallConversionReportingStateEnum
9+
10+
from examples.assets.add_call import add_call_asset, link_asset_to_account, main
11+
from google.ads.googleads.v19.common.types.asset_types import CallAsset
12+
from google.ads.googleads.v19.resources.types.asset import Asset
13+
from google.ads.googleads.v19.resources.types.customer_asset import CustomerAsset
14+
15+
16+
# Define constants for testing
17+
CUSTOMER_ID = "1234567890"
18+
PHONE_NUMBER = "555-555-5555"
19+
COUNTRY_CODE = "US"
20+
CONVERSION_ACTION_ID = "987654321"
21+
AD_GROUP_ID = "111222333"
22+
# Mock resource name for the created asset
23+
CALL_ASSET_RESOURCE_NAME = f"customers/{CUSTOMER_ID}/assets/123"
24+
25+
@pytest.fixture
26+
def mock_google_ads_client():
27+
"""Provides a mock GoogleAdsClient."""
28+
mock_client = MagicMock(spec=GoogleAdsClient)
29+
# Mock get_service
30+
mock_asset_service = MagicMock()
31+
mock_customer_asset_service = MagicMock()
32+
mock_googleads_service = MagicMock()
33+
mock_client.get_service.side_effect = lambda service_name: {
34+
"AssetService": mock_asset_service,
35+
"CustomerAssetService": mock_customer_asset_service,
36+
"GoogleAdsService": mock_googleads_service,
37+
}.get(service_name)
38+
39+
# Mock get_type
40+
def get_type_side_effect(type_name, version=None):
41+
if type_name == "AssetOperation":
42+
return AssetOperation
43+
elif type_name == "CustomerAssetOperation":
44+
return CustomerAssetOperation
45+
elif type_name == "Asset":
46+
return Asset
47+
elif type_name == "CallAsset":
48+
return CallAsset
49+
elif type_name == "CustomerAsset":
50+
return CustomerAsset
51+
raise ValueError(f"Unknown type: {type_name}")
52+
mock_client.get_type.side_effect = get_type_side_effect
53+
54+
# Mock enums
55+
mock_client.enums.AssetFieldTypeEnum = AssetFieldTypeEnum
56+
mock_client.enums.CallConversionReportingStateEnum = CallConversionReportingStateEnum
57+
# Mock conversion_action_path for GoogleAdsService
58+
mock_googleads_service.conversion_action_path.return_value = f"customers/{CUSTOMER_ID}/conversionActions/{CONVERSION_ACTION_ID}"
59+
60+
61+
return mock_client
62+
63+
# Test for add_call_asset function
64+
def test_add_call_asset_without_conversion_action(mock_google_ads_client):
65+
"""Tests add_call_asset when conversion_action_id is None."""
66+
mock_asset_service = mock_google_ads_client.get_service("AssetService")
67+
# Mock the response from mutate_assets
68+
mock_asset_service.mutate_assets.return_value.results = [
69+
MagicMock(resource_name=CALL_ASSET_RESOURCE_NAME)
70+
]
71+
72+
asset_resource_name = add_call_asset(
73+
mock_google_ads_client, CUSTOMER_ID, PHONE_NUMBER, COUNTRY_CODE, None
74+
)
75+
76+
assert asset_resource_name == CALL_ASSET_RESOURCE_NAME
77+
mock_asset_service.mutate_assets.assert_called_once()
78+
args, _ = mock_asset_service.mutate_assets.call_args
79+
assert args[0] == CUSTOMER_ID
80+
# Check the operation
81+
operation = args[1][0] # Operations is a list
82+
assert isinstance(operation, AssetOperation)
83+
assert operation.create is not None
84+
created_asset = operation.create
85+
assert isinstance(created_asset, Asset)
86+
assert created_asset.name == "" # Name is set by the server
87+
assert created_asset.call_asset is not None
88+
call_asset = created_asset.call_asset
89+
assert isinstance(call_asset, CallAsset)
90+
assert call_asset.phone_number == PHONE_NUMBER
91+
assert call_asset.country_code == COUNTRY_CODE
92+
assert not call_asset.call_conversion_action # Should be empty
93+
assert call_asset.call_conversion_reporting_state == CallConversionReportingStateEnum.CallConversionReportingState.USE_ACCOUNT_LEVEL_CALL_CONVERSION_ACTION
94+
95+
96+
def test_add_call_asset_with_conversion_action(mock_google_ads_client):
97+
"""Tests add_call_asset when conversion_action_id is provided."""
98+
mock_asset_service = mock_google_ads_client.get_service("AssetService")
99+
mock_googleads_service = mock_google_ads_client.get_service("GoogleAdsService")
100+
# Mock the response from mutate_assets
101+
mock_asset_service.mutate_assets.return_value.results = [
102+
MagicMock(resource_name=CALL_ASSET_RESOURCE_NAME)
103+
]
104+
expected_conversion_action_path = (
105+
f"customers/{CUSTOMER_ID}/conversionActions/{CONVERSION_ACTION_ID}"
106+
)
107+
mock_googleads_service.conversion_action_path.return_value = (
108+
expected_conversion_action_path
109+
)
110+
111+
asset_resource_name = add_call_asset(
112+
mock_google_ads_client,
113+
CUSTOMER_ID,
114+
PHONE_NUMBER,
115+
COUNTRY_CODE,
116+
CONVERSION_ACTION_ID,
117+
)
118+
119+
assert asset_resource_name == CALL_ASSET_RESOURCE_NAME
120+
mock_asset_service.mutate_assets.assert_called_once()
121+
args, _ = mock_asset_service.mutate_assets.call_args
122+
assert args[0] == CUSTOMER_ID
123+
# Check the operation
124+
operation = args[1][0]
125+
assert isinstance(operation, AssetOperation)
126+
assert operation.create is not None
127+
created_asset = operation.create
128+
assert isinstance(created_asset, Asset)
129+
assert created_asset.call_asset is not None
130+
call_asset = created_asset.call_asset
131+
assert isinstance(call_asset, CallAsset)
132+
assert call_asset.phone_number == PHONE_NUMBER
133+
assert call_asset.country_code == COUNTRY_CODE
134+
assert call_asset.call_conversion_action == expected_conversion_action_path
135+
assert call_asset.call_conversion_reporting_state == CallConversionReportingStateEnum.CallConversionReportingState.USE_RESOURCE_LEVEL_CALL_CONVERSION_ACTION
136+
mock_googleads_service.conversion_action_path.assert_called_once_with(
137+
CUSTOMER_ID, CONVERSION_ACTION_ID
138+
)
139+
140+
# Test for link_asset_to_account function
141+
def test_link_asset_to_account(mock_google_ads_client):
142+
"""Tests the link_asset_to_account function."""
143+
mock_customer_asset_service = mock_google_ads_client.get_service(
144+
"CustomerAssetService"
145+
)
146+
# Mock the response from mutate_customer_assets
147+
mock_customer_asset_service.mutate_customer_assets.return_value.results = [
148+
MagicMock(resource_name=f"customers/{CUSTOMER_ID}/customerAssets/123_CALL")
149+
]
150+
151+
link_asset_to_account(
152+
mock_google_ads_client, CUSTOMER_ID, CALL_ASSET_RESOURCE_NAME
153+
)
154+
155+
mock_customer_asset_service.mutate_customer_assets.assert_called_once()
156+
args, _ = mock_customer_asset_service.mutate_customer_assets.call_args
157+
assert args[0] == CUSTOMER_ID
158+
# Check the operation
159+
operation = args[1][0] # Operations is a list
160+
assert isinstance(operation, CustomerAssetOperation)
161+
assert operation.create is not None
162+
created_customer_asset = operation.create
163+
assert isinstance(created_customer_asset, CustomerAsset)
164+
assert created_customer_asset.asset == CALL_ASSET_RESOURCE_NAME
165+
assert created_customer_asset.field_type == AssetFieldTypeEnum.AssetFieldType.CALL
166+
167+
168+
# Tests for main function and argument parsing
169+
@patch("sys.argv", [
170+
"examples/assets/add_call.py",
171+
f"--customer_id={CUSTOMER_ID}",
172+
f"--phone_number={PHONE_NUMBER}",
173+
f"--country_code={COUNTRY_CODE}",
174+
])
175+
@patch("examples.assets.add_call.GoogleAdsClient.load_from_storage")
176+
@patch("examples.assets.add_call.add_call_asset")
177+
@patch("examples.assets.add_call.link_asset_to_account")
178+
def test_main_without_conversion_action_id(
179+
mock_link_asset_to_account,
180+
mock_add_call_asset,
181+
mock_load_from_storage,
182+
mock_google_ads_client, # Use the fixture for the client
183+
capsys,
184+
):
185+
"""Tests the main function when conversion_action_id is not provided."""
186+
mock_load_from_storage.return_value = mock_google_ads_client
187+
mock_add_call_asset.return_value = CALL_ASSET_RESOURCE_NAME
188+
189+
main()
190+
191+
mock_load_from_storage.assert_called_once_with(version="v19")
192+
mock_add_call_asset.assert_called_once_with(
193+
mock_google_ads_client,
194+
CUSTOMER_ID,
195+
PHONE_NUMBER,
196+
COUNTRY_CODE,
197+
None, # conversion_action_id
198+
None, # ad_group_id - not testing this here
199+
)
200+
mock_link_asset_to_account.assert_called_once_with(
201+
mock_google_ads_client, CUSTOMER_ID, CALL_ASSET_RESOURCE_NAME, None # ad_group_id
202+
)
203+
# Check stdout for printed messages (optional, but good practice)
204+
captured = capsys.readouterr()
205+
assert f"Created call asset with resource name '{CALL_ASSET_RESOURCE_NAME}'" in captured.out
206+
assert f"Linked call asset to customer '{CUSTOMER_ID}'." in captured.out
207+
208+
209+
@patch("sys.argv", [
210+
"examples/assets/add_call.py",
211+
f"--customer_id={CUSTOMER_ID}",
212+
f"--phone_number={PHONE_NUMBER}",
213+
f"--country_code={COUNTRY_CODE}",
214+
f"--conversion_action_id={CONVERSION_ACTION_ID}",
215+
])
216+
@patch("examples.assets.add_call.GoogleAdsClient.load_from_storage")
217+
@patch("examples.assets.add_call.add_call_asset")
218+
@patch("examples.assets.add_call.link_asset_to_account")
219+
def test_main_with_conversion_action_id(
220+
mock_link_asset_to_account,
221+
mock_add_call_asset,
222+
mock_load_from_storage,
223+
mock_google_ads_client, # Use the fixture
224+
capsys,
225+
):
226+
"""Tests the main function when conversion_action_id is provided."""
227+
mock_load_from_storage.return_value = mock_google_ads_client
228+
mock_add_call_asset.return_value = CALL_ASSET_RESOURCE_NAME
229+
230+
main()
231+
232+
mock_load_from_storage.assert_called_once_with(version="v19")
233+
mock_add_call_asset.assert_called_once_with(
234+
mock_google_ads_client,
235+
CUSTOMER_ID,
236+
PHONE_NUMBER,
237+
COUNTRY_CODE,
238+
CONVERSION_ACTION_ID,
239+
None, # ad_group_id
240+
)
241+
mock_link_asset_to_account.assert_called_once_with(
242+
mock_google_ads_client, CUSTOMER_ID, CALL_ASSET_RESOURCE_NAME, None # ad_group_id
243+
)
244+
captured = capsys.readouterr()
245+
assert f"Created call asset with resource name '{CALL_ASSET_RESOURCE_NAME}'" in captured.out
246+
assert f"Linked call asset to customer '{CUSTOMER_ID}'." in captured.out
247+
248+
249+
@patch("sys.argv", [
250+
"examples/assets/add_call.py",
251+
f"--customer_id={CUSTOMER_ID}",
252+
f"--phone_number={PHONE_NUMBER}",
253+
f"--country_code={COUNTRY_CODE}",
254+
f"--ad_group_id={AD_GROUP_ID}",
255+
])
256+
@patch("examples.assets.add_call.GoogleAdsClient.load_from_storage")
257+
@patch("examples.assets.add_call.add_call_asset")
258+
@patch("examples.assets.add_call.link_asset_to_account") # This should be link_asset_to_ad_group
259+
@patch("examples.assets.add_call.link_asset_to_ad_group") # Add this mock
260+
def test_main_with_ad_group_id(
261+
mock_link_asset_to_ad_group, # Corrected mock name
262+
mock_link_asset_to_account, # Keep this for now, though it might not be called
263+
mock_add_call_asset,
264+
mock_load_from_storage,
265+
mock_google_ads_client, # Use the fixture
266+
capsys,
267+
):
268+
"""Tests the main function when ad_group_id is provided."""
269+
mock_load_from_storage.return_value = mock_google_ads_client
270+
mock_add_call_asset.return_value = CALL_ASSET_RESOURCE_NAME
271+
272+
main()
273+
274+
mock_load_from_storage.assert_called_once_with(version="v19")
275+
mock_add_call_asset.assert_called_once_with(
276+
mock_google_ads_client,
277+
CUSTOMER_ID,
278+
PHONE_NUMBER,
279+
COUNTRY_CODE,
280+
None, # conversion_action_id
281+
AD_GROUP_ID,
282+
)
283+
# Assert that link_asset_to_account was NOT called
284+
mock_link_asset_to_account.assert_not_called()
285+
# Assert that link_asset_to_ad_group was called
286+
mock_link_asset_to_ad_group.assert_called_once_with(
287+
mock_google_ads_client, CUSTOMER_ID, CALL_ASSET_RESOURCE_NAME, AD_GROUP_ID
288+
)
289+
captured = capsys.readouterr()
290+
assert f"Created call asset with resource name '{CALL_ASSET_RESOURCE_NAME}'" in captured.out
291+
assert f"Linked call asset to ad group '{AD_GROUP_ID}'." in captured.out

0 commit comments

Comments
 (0)