/ tests / test_google_ads_mappers.py
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