|
| 1 | +import argparse |
| 2 | +import sys |
| 3 | +import unittest |
| 4 | +from io import StringIO |
| 5 | +from unittest.mock import MagicMock, patch |
| 6 | + |
| 7 | +from google.ads.googleads.errors import GoogleAdsException # Import GoogleAdsException |
| 8 | +from google.ads.googleads.v19.common.types.ad_asset import AdTextAsset # Corrected import |
| 9 | +from google.ads.googleads.v19.enums.types.ad_group_ad_status import AdGroupAdStatusEnum |
| 10 | +from google.ads.googleads.v19.enums.types.served_asset_field_type import ServedAssetFieldTypeEnum |
| 11 | +from google.ads.googleads.v19.resources.types.ad import Ad |
| 12 | +from google.ads.googleads.v19.resources.types.ad_group_ad import AdGroupAd |
| 13 | +from google.ads.googleads.v19.services.services.google_ads_service import ( |
| 14 | + GoogleAdsServiceClient, |
| 15 | +) |
| 16 | +from google.ads.googleads.v19.services.types.google_ads_service import ( |
| 17 | + GoogleAdsRow, |
| 18 | + SearchGoogleAdsResponse, |
| 19 | +) |
| 20 | + |
| 21 | +# Add the parent directory to the Python path to allow importing from sibling directories |
| 22 | +sys.path.append("../..") |
| 23 | + |
| 24 | +from basic_operations.get_responsive_search_ads import ( |
| 25 | + main, |
| 26 | + ad_text_assets_to_strs, |
| 27 | +) |
| 28 | + |
| 29 | + |
| 30 | +class TestGetResponsiveSearchAds(unittest.TestCase): |
| 31 | + def test_ad_text_assets_to_strs(self): |
| 32 | + assets = [] |
| 33 | + asset1 = AdTextAsset() |
| 34 | + asset1.text = "Headline 1" |
| 35 | + asset1.pinned_field = ServedAssetFieldTypeEnum.ServedAssetFieldType.HEADLINE_1 |
| 36 | + assets.append(asset1) |
| 37 | + |
| 38 | + asset2 = AdTextAsset() |
| 39 | + asset2.text = "Description 1" |
| 40 | + asset2.pinned_field = ServedAssetFieldTypeEnum.ServedAssetFieldType.DESCRIPTION_1 |
| 41 | + assets.append(asset2) |
| 42 | + |
| 43 | + expected_output = [ |
| 44 | + "\t Headline 1 pinned to HEADLINE_1", |
| 45 | + "\t Description 1 pinned to DESCRIPTION_1", |
| 46 | + ] |
| 47 | + self.assertEqual(ad_text_assets_to_strs(assets), expected_output) |
| 48 | + |
| 49 | + @patch("basic_operations.get_responsive_search_ads.GoogleAdsClient") |
| 50 | + def test_main_no_ads_found(self, mock_google_ads_client_constructor): |
| 51 | + mock_ads_client = MagicMock() |
| 52 | + mock_google_ads_service = MagicMock(spec=GoogleAdsServiceClient) |
| 53 | + mock_ads_client.get_service.return_value = mock_google_ads_service |
| 54 | + |
| 55 | + # Configure the mock GoogleAdsServiceClient to return an empty list of results |
| 56 | + mock_google_ads_service.search.return_value = SearchGoogleAdsResponse() |
| 57 | + |
| 58 | + mock_google_ads_client_constructor.load_from_storage.return_value = ( |
| 59 | + mock_ads_client |
| 60 | + ) |
| 61 | + |
| 62 | + # Redirect stdout to capture print statements |
| 63 | + captured_output = StringIO() |
| 64 | + sys.stdout = captured_output |
| 65 | + |
| 66 | + main(mock_ads_client, "test_customer_id") |
| 67 | + |
| 68 | + sys.stdout = sys.__stdout__ # Reset stdout |
| 69 | + self.assertIn( |
| 70 | + "No responsive search ads were found.", captured_output.getvalue() |
| 71 | + ) |
| 72 | + mock_google_ads_service.search.assert_called_once() |
| 73 | + |
| 74 | + @patch("basic_operations.get_responsive_search_ads.GoogleAdsClient") |
| 75 | + def test_main_ads_found(self, mock_google_ads_client_constructor): |
| 76 | + mock_ads_client = MagicMock() |
| 77 | + mock_google_ads_service = MagicMock(spec=GoogleAdsServiceClient) |
| 78 | + mock_ads_client.get_service.return_value = mock_google_ads_service |
| 79 | + |
| 80 | + # Create mock ad data |
| 81 | + row1 = GoogleAdsRow() |
| 82 | + row1.ad_group_ad.ad.resource_name = "customers/123/ads/1" |
| 83 | + row1.ad_group_ad.status = AdGroupAdStatusEnum.AdGroupAdStatus.ENABLED |
| 84 | + # Headlines |
| 85 | + headline1 = AdTextAsset() |
| 86 | + headline1.text = "Test Headline 1" |
| 87 | + headline1.pinned_field = ServedAssetFieldTypeEnum.ServedAssetFieldType.HEADLINE_1 |
| 88 | + row1.ad_group_ad.ad.responsive_search_ad.headlines.extend([headline1]) |
| 89 | + # Descriptions |
| 90 | + description1 = AdTextAsset() |
| 91 | + description1.text = "Test Description 1" |
| 92 | + description1.pinned_field = ServedAssetFieldTypeEnum.ServedAssetFieldType.DESCRIPTION_1 |
| 93 | + row1.ad_group_ad.ad.responsive_search_ad.descriptions.extend([description1]) |
| 94 | + |
| 95 | + row2 = GoogleAdsRow() |
| 96 | + row2.ad_group_ad.ad.resource_name = "customers/123/ads/2" |
| 97 | + row2.ad_group_ad.status = AdGroupAdStatusEnum.AdGroupAdStatus.PAUSED |
| 98 | + headline2 = AdTextAsset() |
| 99 | + headline2.text = "Another Headline" |
| 100 | + headline2.pinned_field = ServedAssetFieldTypeEnum.ServedAssetFieldType.UNSPECIFIED |
| 101 | + row2.ad_group_ad.ad.responsive_search_ad.headlines.extend([headline2]) |
| 102 | + |
| 103 | + |
| 104 | + mock_response = SearchGoogleAdsResponse() |
| 105 | + mock_response.results.extend([row1, row2]) |
| 106 | + mock_google_ads_service.search.return_value = mock_response |
| 107 | + mock_google_ads_client_constructor.load_from_storage.return_value = ( |
| 108 | + mock_ads_client |
| 109 | + ) |
| 110 | + |
| 111 | + captured_output = StringIO() |
| 112 | + sys.stdout = captured_output |
| 113 | + |
| 114 | + main(mock_ads_client, "test_customer_id", ad_group_id="test_ad_group_id") |
| 115 | + |
| 116 | + sys.stdout = sys.__stdout__ # Reset stdout |
| 117 | + output_text = captured_output.getvalue() |
| 118 | + |
| 119 | + self.assertIn( |
| 120 | + 'Responsive search ad with resource name "customers/123/ads/1", status ENABLED was found.', |
| 121 | + output_text, |
| 122 | + ) |
| 123 | + self.assertIn("Headlines:\n\t Test Headline 1 pinned to HEADLINE_1", output_text) |
| 124 | + self.assertIn("Descriptions:\n\t Test Description 1 pinned to DESCRIPTION_1", output_text) |
| 125 | + |
| 126 | + self.assertIn( |
| 127 | + 'Responsive search ad with resource name "customers/123/ads/2", status PAUSED was found.', |
| 128 | + output_text, |
| 129 | + ) |
| 130 | + self.assertIn("Headlines:\n\t Another Headline pinned to UNSPECIFIED", output_text) |
| 131 | + # Check if the query was modified for ad_group_id |
| 132 | + self.assertTrue( |
| 133 | + "AND ad_group.id = test_ad_group_id" |
| 134 | + in mock_google_ads_service.search.call_args[1]["request"].query |
| 135 | + ) |
| 136 | + |
| 137 | + @patch("basic_operations.get_responsive_search_ads.GoogleAdsClient") |
| 138 | + # Removed mock_sys_exit as main() itself doesn't call sys.exit() |
| 139 | + def test_main_google_ads_exception(self, mock_google_ads_client_constructor): |
| 140 | + mock_ads_client = MagicMock() |
| 141 | + mock_google_ads_service = MagicMock(spec=GoogleAdsServiceClient) |
| 142 | + mock_ads_client.get_service.return_value = mock_google_ads_service |
| 143 | + |
| 144 | + # Configure the mock GoogleAdsServiceClient to raise an exception |
| 145 | + # Use a real GoogleAdsException for type checking if possible, |
| 146 | + # but for this test, ensuring main re-raises is key. |
| 147 | + # We'll use the helper that creates a MagicMock that looks like one. |
| 148 | + mock_exception_to_raise = self._create_mock_google_ads_exception() |
| 149 | + mock_google_ads_service.search.side_effect = mock_exception_to_raise |
| 150 | + |
| 151 | + mock_google_ads_client_constructor.load_from_storage.return_value = ( |
| 152 | + mock_ads_client |
| 153 | + ) |
| 154 | + |
| 155 | + # Assert that calling main raises the GoogleAdsException (or the mock equivalent) |
| 156 | + with self.assertRaises(GoogleAdsException) as context: # Or type(mock_exception_to_raise) |
| 157 | + main(mock_ads_client, "test_customer_id") |
| 158 | + |
| 159 | + # Optionally, assert details about the raised exception |
| 160 | + # self.assertEqual(context.exception.request_id, "mock_request_id") |
| 161 | + |
| 162 | + |
| 163 | + def _create_mock_google_ads_exception(self): |
| 164 | + """Helper method to create a mock GoogleAdsException. |
| 165 | + Ideally, this would be a real GoogleAdsException instance, but |
| 166 | + constructing one can be complex. A MagicMock is used for simplicity here |
| 167 | + if we are only checking that an exception of this type is raised. |
| 168 | + For more detailed checks on the exception's attributes, a more realistic |
| 169 | + mock or a real instance would be better. |
| 170 | + """ |
| 171 | + # For this test, we need an object that is an instance of GoogleAdsException. |
| 172 | + # We will instantiate GoogleAdsException directly. |
| 173 | + # The constructor typically takes: error, failure, request_id, call. |
| 174 | + # We'll use MagicMocks for the complex protobuf parts if needed by the constructor, |
| 175 | + # but the key is that the object itself is a proper exception. |
| 176 | + |
| 177 | + # These mocks are for the attributes that GoogleAdsException might store |
| 178 | + # and that the calling code (the main script's error handler) might access. |
| 179 | + mock_google_ads_error = MagicMock() # Represents the services.GoogleAdsError |
| 180 | + # If GoogleAdsError has a code() method that returns an object with a name attribute: |
| 181 | + mock_error_code = MagicMock() |
| 182 | + mock_error_code.name = "UNKNOWN_ERROR_FOR_TEST" |
| 183 | + mock_google_ads_error.code.return_value = mock_error_code |
| 184 | + |
| 185 | + mock_google_ads_failure = MagicMock() # Represents the errors.GoogleAdsFailure |
| 186 | + mock_error_detail = MagicMock() |
| 187 | + mock_error_detail.message = "Mocked error detail message" |
| 188 | + mock_field_path_element = MagicMock() |
| 189 | + mock_field_path_element.field_name = "mock_field_in_failure" |
| 190 | + mock_error_detail.location.field_path_elements = [mock_field_path_element] |
| 191 | + mock_google_ads_failure.errors = [mock_error_detail] |
| 192 | + |
| 193 | + # Instantiate the actual GoogleAdsException |
| 194 | + # The `call` argument is often a grpc.Call instance, using a mock for it. |
| 195 | + ex = GoogleAdsException( |
| 196 | + error=mock_google_ads_error, # This should be a GoogleAdsError instance |
| 197 | + failure=mock_google_ads_failure, # This should be a GoogleAdsFailure instance |
| 198 | + request_id="mock_request_id_real_ex", |
| 199 | + call=MagicMock() # Mocking the gRPC call object |
| 200 | + ) |
| 201 | + return ex |
| 202 | + |
| 203 | + |
| 204 | +if __name__ == "__main__": |
| 205 | + unittest.main() |
0 commit comments