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