test_google_ads_mappers.py
1 """Google Ads mappers テスト 2 3 mappers.pyの各関数にモックデータを渡して正しく変換されることを確認する。 4 """ 5 6 from __future__ import annotations 7 8 from unittest.mock import MagicMock 9 10 import pytest 11 from google.ads.googleads.v23.enums.types.bidding_strategy_type import ( 12 BiddingStrategyTypeEnum, 13 ) 14 15 from mureo.google_ads.mappers import ( 16 _BIDDING_STRATEGY_MAP, 17 _micros_to_currency, 18 _safe_float, 19 _safe_int, 20 _safe_str, 21 map_ad_group, 22 map_ad_performance_report, 23 map_approval_status, 24 map_bidding_strategy_type, 25 map_bidding_system_status, 26 map_callout, 27 map_campaign, 28 map_change_event, 29 map_conversion_action, 30 map_criterion_approval_status, 31 map_entity_status, 32 map_keyword, 33 map_keyword_quality_info, 34 map_negative_keyword, 35 map_performance_report, 36 map_primary_status, 37 map_primary_status_reason, 38 map_recommendation, 39 map_review_status, 40 map_search_term, 41 map_serving_status, 42 map_sitelink, 43 map_tag_snippet, 44 ) 45 46 47 # --------------------------------------------------------------------------- 48 # ヘルパー関数 49 # --------------------------------------------------------------------------- 50 51 52 @pytest.mark.unit 53 class TestMicrosToCurrency: 54 def test_正常変換(self) -> None: 55 assert _micros_to_currency(1_000_000) == 1.0 56 57 def test_ゼロ(self) -> None: 58 assert _micros_to_currency(0) == 0.0 59 60 61 @pytest.mark.unit 62 class TestSafeInt: 63 def test_属性あり(self) -> None: 64 obj = MagicMock() 65 obj.impressions = 100 66 assert _safe_int(obj, "impressions") == 100 67 68 def test_属性なし(self) -> None: 69 obj = MagicMock(spec=[]) 70 assert _safe_int(obj, "impressions") == 0 71 72 73 @pytest.mark.unit 74 class TestSafeFloat: 75 def test_属性あり(self) -> None: 76 obj = MagicMock() 77 obj.ctr = 0.05 78 assert _safe_float(obj, "ctr") == 0.05 79 80 def test_属性なし(self) -> None: 81 obj = MagicMock(spec=[]) 82 assert _safe_float(obj, "ctr") == 0.0 83 84 85 @pytest.mark.unit 86 class TestSafeStr: 87 def test_属性あり(self) -> None: 88 obj = MagicMock() 89 obj.name = "test" 90 assert _safe_str(obj, "name") == "test" 91 92 def test_属性なし(self) -> None: 93 obj = MagicMock(spec=[]) 94 assert _safe_str(obj, "name") == "" 95 96 97 # --------------------------------------------------------------------------- 98 # enum変換 99 # --------------------------------------------------------------------------- 100 101 102 @pytest.mark.unit 103 class TestEnumMappers: 104 def test_map_entity_status_enabled(self) -> None: 105 assert map_entity_status(2) == "ENABLED" 106 107 def test_map_entity_status_paused(self) -> None: 108 assert map_entity_status(3) == "PAUSED" 109 110 def test_map_entity_status_string(self) -> None: 111 assert map_entity_status("ENABLED") == "ENABLED" 112 113 def test_map_serving_status_serving(self) -> None: 114 assert map_serving_status(2) == "SERVING" 115 116 def test_map_approval_status_approved(self) -> None: 117 assert map_approval_status(4) == "APPROVED" 118 119 def test_map_review_status_reviewed(self) -> None: 120 assert map_review_status(3) == "REVIEWED" 121 122 def test_map_primary_status_eligible(self) -> None: 123 assert map_primary_status(2) == "ELIGIBLE" 124 125 def test_map_primary_status_learning(self) -> None: 126 assert map_primary_status(9) == "LEARNING" 127 128 def test_map_bidding_strategy_type_maximize_clicks(self) -> None: 129 """v23でTARGET_SPENDに統合されたMAXIMIZE_CLICKSが正しく返る""" 130 assert _BIDDING_STRATEGY_MAP[9] == "MAXIMIZE_CLICKS" 131 132 def test_map_criterion_approval_status_int(self) -> None: 133 # APPROVED = 2 for AdGroupCriterionApprovalStatus 134 result = map_criterion_approval_status(2) 135 assert result == "APPROVED" 136 137 def test_map_bidding_system_status_int(self) -> None: 138 result = map_bidding_system_status(0) 139 assert result == "UNSPECIFIED" 140 141 def test_map_primary_status_reason_int(self) -> None: 142 result = map_primary_status_reason(0) 143 assert result == "UNSPECIFIED" 144 145 146 # --------------------------------------------------------------------------- 147 # エンティティ変換 148 # --------------------------------------------------------------------------- 149 150 151 @pytest.mark.unit 152 class TestMapCampaign: 153 def test_基本変換(self) -> None: 154 campaign = MagicMock() 155 campaign.id = 12345 156 campaign.name = "テストキャンペーン" 157 campaign.status = 2 158 campaign.campaign_budget = 5_000_000 159 campaign.bidding_strategy_type = "TARGET_CPA" 160 161 result = map_campaign(campaign) 162 163 assert result["id"] == "12345" 164 assert result["name"] == "テストキャンペーン" 165 assert result["status"] == "ENABLED" 166 assert result["budget_amount_micros"] == 5_000_000 167 168 def test_オプションフィールド付き(self) -> None: 169 campaign = MagicMock() 170 campaign.id = 1 171 campaign.name = "C1" 172 campaign.status = 3 173 campaign.campaign_budget = 0 174 campaign.bidding_strategy_type = 2 175 campaign.serving_status = 2 176 campaign.primary_status = 9 177 campaign.primary_status_reasons = [0] 178 campaign.bidding_strategy_system_status = 0 179 campaign.start_date = "2024-01-01" 180 campaign.end_date = "2024-12-31" 181 182 result = map_campaign(campaign) 183 184 assert result["serving_status"] == "SERVING" 185 assert result["primary_status"] == "LEARNING" 186 assert result["start_date"] == "2024-01-01" 187 assert result["end_date"] == "2024-12-31" 188 189 def test_advertising_channel_type_SEARCH(self) -> None: 190 """advertising_channel_type が "SEARCH" として返ること。""" 191 campaign = MagicMock() 192 campaign.id = 100 193 campaign.name = "Search Campaign" 194 campaign.status = 2 195 campaign.campaign_budget = 0 196 campaign.bidding_strategy_type = 0 197 campaign.advertising_channel_type = 2 # SEARCH 198 199 result = map_campaign(campaign) 200 assert result["channel_type"] == "SEARCH" 201 202 def test_advertising_channel_type_DISPLAY(self) -> None: 203 """advertising_channel_type が "DISPLAY" として返ること。""" 204 campaign = MagicMock() 205 campaign.id = 200 206 campaign.name = "Display Campaign" 207 campaign.status = 2 208 campaign.campaign_budget = 0 209 campaign.bidding_strategy_type = 0 210 campaign.advertising_channel_type = 3 # DISPLAY 211 212 result = map_campaign(campaign) 213 assert result["channel_type"] == "DISPLAY" 214 215 216 @pytest.mark.unit 217 class TestMapAdGroup: 218 def test_基本変換(self) -> None: 219 ad_group = MagicMock() 220 ad_group.id = 67890 221 ad_group.name = "テスト広告グループ" 222 ad_group.status = 2 223 ad_group.campaign = "customers/123/campaigns/456" 224 ad_group.cpc_bid_micros = 100_000_000 225 226 result = map_ad_group(ad_group) 227 228 assert result["id"] == "67890" 229 assert result["name"] == "テスト広告グループ" 230 assert result["status"] == "ENABLED" 231 232 def test_キャンペーン情報付き(self) -> None: 233 ad_group = MagicMock() 234 ad_group.id = 1 235 ad_group.name = "AG1" 236 ad_group.status = 2 237 238 campaign = MagicMock() 239 campaign.id = 999 240 campaign.name = "C999" 241 campaign.status = 2 242 243 result = map_ad_group(ad_group, campaign) 244 245 assert result["campaign_id"] == "999" 246 assert result["campaign_name"] == "C999" 247 assert result["campaign_status"] == "ENABLED" 248 249 250 @pytest.mark.unit 251 class TestMapKeyword: 252 def test_基本変換(self) -> None: 253 keyword = MagicMock() 254 keyword.criterion_id = 11111 255 keyword.keyword.text = "ランニングシューズ" 256 keyword.keyword.match_type = "BROAD" 257 keyword.status = 2 258 keyword.approval_status = 2 # APPROVED (criterion) 259 260 result = map_keyword(keyword) 261 262 assert result["text"] == "ランニングシューズ" 263 assert result["match_type"] == "BROAD" 264 assert result["status"] == "ENABLED" 265 assert result["approval_status"] == "APPROVED" 266 267 def test_approval_statusなし(self) -> None: 268 keyword = MagicMock(spec=["criterion_id", "keyword", "status"]) 269 keyword.criterion_id = 22222 270 keyword.keyword.text = "テスト" 271 keyword.keyword.match_type = "EXACT" 272 keyword.status = 2 273 274 result = map_keyword(keyword) 275 276 assert "approval_status" not in result 277 278 def test_approval_status_unspecified(self) -> None: 279 """approval_status=0(UNSPECIFIED)でもマッピングされる""" 280 keyword = MagicMock() 281 keyword.criterion_id = 33333 282 keyword.keyword.text = "テスト" 283 keyword.keyword.match_type = "EXACT" 284 keyword.status = 2 285 keyword.approval_status = 0 286 287 result = map_keyword(keyword) 288 289 assert result["approval_status"] == "UNSPECIFIED" 290 291 def test_キャンペーン_広告グループ情報付き(self) -> None: 292 keyword = MagicMock() 293 keyword.criterion_id = 44444 294 keyword.keyword.text = "テスト" 295 keyword.keyword.match_type = "PHRASE" 296 keyword.status = 2 297 keyword.approval_status = 2 298 299 campaign = MagicMock() 300 campaign.id = 100 301 campaign.name = "C100" 302 303 ad_group = MagicMock() 304 ad_group.id = 200 305 ad_group.name = "AG200" 306 307 result = map_keyword(keyword, campaign, ad_group) 308 309 assert result["campaign_id"] == "100" 310 assert result["campaign_name"] == "C100" 311 assert result["ad_group_id"] == "200" 312 assert result["ad_group_name"] == "AG200" 313 314 315 @pytest.mark.unit 316 class TestMapKeywordQualityInfo: 317 def test_品質スコア付きキーワード(self) -> None: 318 keyword = MagicMock() 319 keyword.criterion_id = 55555 320 keyword.keyword.text = "テスト" 321 keyword.keyword.match_type = "BROAD" 322 keyword.status = 2 323 keyword.approval_status = 2 324 keyword.system_serving_status = 2 # ELIGIBLE 325 keyword.quality_info.quality_score = 7 326 keyword.quality_info.creative_quality_score = 3 # AVERAGE 327 keyword.quality_info.post_click_quality_score = 4 # ABOVE_AVERAGE 328 keyword.quality_info.search_predicted_ctr = 2 # BELOW_AVERAGE 329 330 result = map_keyword_quality_info(keyword) 331 332 assert result["quality_score"] == 7 333 assert result["creative_quality_score"] == "AVERAGE" 334 assert result["post_click_quality_score"] == "ABOVE_AVERAGE" 335 assert result["search_predicted_ctr"] == "BELOW_AVERAGE" 336 assert result["system_serving_status"] == "ELIGIBLE" 337 338 def test_品質情報なし(self) -> None: 339 keyword = MagicMock(spec=["criterion_id", "keyword", "status"]) 340 keyword.criterion_id = 66666 341 keyword.keyword.text = "テスト" 342 keyword.keyword.match_type = "EXACT" 343 keyword.status = 2 344 345 result = map_keyword_quality_info(keyword) 346 347 assert result["quality_score"] is None 348 assert result["creative_quality_score"] == "UNSPECIFIED" 349 350 351 @pytest.mark.unit 352 class TestMapPerformanceReport: 353 def test_基本変換(self) -> None: 354 row = MagicMock() 355 row.campaign.name = "テストキャンペーン" 356 row.campaign.id = 123 357 row.metrics.impressions = 1000 358 row.metrics.clicks = 50 359 row.metrics.cost_micros = 5_000_000 360 row.metrics.conversions = 3.0 361 row.metrics.ctr = 0.05 362 row.metrics.average_cpc = 100_000 363 row.metrics.cost_per_conversion = 1_666_667 364 365 result = map_performance_report([row]) 366 367 assert len(result) == 1 368 assert result[0]["campaign_name"] == "テストキャンペーン" 369 assert result[0]["metrics"]["impressions"] == 1000 370 assert result[0]["metrics"]["clicks"] == 50 371 assert result[0]["metrics"]["cost"] == 5.0 372 373 374 @pytest.mark.unit 375 class TestMapNegativeKeyword: 376 def test_基本変換(self) -> None: 377 criterion = MagicMock() 378 criterion.criterion_id = 77777 379 criterion.keyword.text = "無料" 380 criterion.keyword.match_type = "EXACT" 381 382 result = map_negative_keyword(criterion) 383 384 assert result["criterion_id"] == "77777" 385 assert result["keyword_text"] == "無料" 386 assert result["match_type"] == "EXACT" 387 388 389 @pytest.mark.unit 390 class TestMapSearchTerm: 391 def test_基本変換(self) -> None: 392 row = MagicMock() 393 row.search_term_view.search_term = "テスト検索語句" 394 row.metrics.impressions = 500 395 row.metrics.clicks = 25 396 row.metrics.cost_micros = 2_500_000 397 row.metrics.conversions = 1.0 398 row.metrics.ctr = 0.05 399 400 result = map_search_term(row) 401 402 assert result["search_term"] == "テスト検索語句" 403 assert result["metrics"]["impressions"] == 500 404 assert result["metrics"]["cost"] == 2.5 405 406 407 @pytest.mark.unit 408 class TestMapSitelink: 409 def test_基本変換(self) -> None: 410 asset = MagicMock() 411 asset.asset.id = 88888 412 asset.asset.resource_name = "customers/123/assets/88888" 413 asset.asset.sitelink_asset.link_text = "詳細はこちら" 414 asset.asset.sitelink_asset.description1 = "説明1" 415 asset.asset.sitelink_asset.description2 = "説明2" 416 asset.asset.final_urls = ["https://example.com"] 417 418 result = map_sitelink(asset) 419 420 assert result["id"] == "88888" 421 assert result["link_text"] == "詳細はこちら" 422 assert result["final_urls"] == ["https://example.com"] 423 424 425 @pytest.mark.unit 426 class TestMapCallout: 427 def test_基本変換(self) -> None: 428 asset = MagicMock() 429 asset.asset.id = 99999 430 asset.asset.resource_name = "customers/123/assets/99999" 431 asset.asset.callout_asset.callout_text = "送料無料" 432 433 result = map_callout(asset) 434 435 assert result["id"] == "99999" 436 assert result["callout_text"] == "送料無料" 437 438 439 @pytest.mark.unit 440 class TestMapConversionAction: 441 def test_基本変換(self) -> None: 442 action = MagicMock() 443 action.id = 10001 444 action.name = "購入完了" 445 action.type_ = "WEBPAGE" 446 action.status = 2 447 action.category = "PURCHASE" 448 449 result = map_conversion_action(action) 450 451 assert result["id"] == "10001" 452 assert result["name"] == "購入完了" 453 assert result["status"] == "ENABLED" 454 455 456 @pytest.mark.unit 457 class TestMapTagSnippet: 458 def test_基本変換(self) -> None: 459 snippet = MagicMock() 460 snippet.type_ = "PAGE_LOAD" 461 snippet.page_header = "<header>" 462 snippet.event_snippet = "<event>" 463 464 result = map_tag_snippet(snippet) 465 466 assert result["type"] == "PAGE_LOAD" 467 assert result["page_header"] == "<header>" 468 469 470 @pytest.mark.unit 471 class TestMapRecommendation: 472 def test_基本変換(self) -> None: 473 rec = MagicMock() 474 rec.resource_name = "customers/123/recommendations/456" 475 rec.type_ = "KEYWORD" 476 rec.impact.base_metrics.impressions = 1000.0 477 rec.impact.base_metrics.clicks = 50.0 478 rec.impact.base_metrics.cost_micros = 100000 479 rec.campaign = "customers/123/campaigns/789" 480 481 result = map_recommendation(rec) 482 483 assert result["resource_name"] == "customers/123/recommendations/456" 484 assert result["impact"]["base_metrics"]["impressions"] == 1000.0 485 486 487 @pytest.mark.unit 488 class TestMapChangeEvent: 489 def test_基本変換(self) -> None: 490 event = MagicMock() 491 event.change_date_time = "2024-01-01 12:00:00" 492 event.change_resource_type = "CAMPAIGN" 493 event.resource_change_operation = "UPDATE" 494 event.changed_fields.paths = ["budget"] 495 event.user_email = "test@example.com" 496 497 result = map_change_event(event) 498 499 assert result["change_date_time"] == "2024-01-01 12:00:00" 500 assert result["change_resource_type"] == "CAMPAIGN" 501 assert result["changed_fields"] == ["budget"] 502 503 504 @pytest.mark.unit 505 class TestMapAdPerformanceReport: 506 def test_基本変換(self) -> None: 507 row = MagicMock() 508 row.ad_group_ad.ad.id = 111 509 row.ad_group_ad.ad.type_ = 3 # RSA 510 row.ad_group_ad.status = 2 511 row.ad_group.id = 222 512 row.ad_group.name = "AG" 513 row.campaign.id = 333 514 row.campaign.name = "C" 515 row.metrics.impressions = 100 516 row.metrics.clicks = 10 517 row.metrics.cost_micros = 1_000_000 518 row.metrics.conversions = 1.0 519 row.metrics.ctr = 0.1 520 row.metrics.average_cpc = 100_000 521 row.metrics.cost_per_conversion = 1_000_000 522 523 result = map_ad_performance_report([row]) 524 525 assert len(result) == 1 526 assert result[0]["ad_id"] == "111" 527 assert result[0]["campaign_name"] == "C" 528 assert result[0]["metrics"]["cost"] == 1.0