/ tests / test_meta_ads_mappers.py
test_meta_ads_mappers.py
  1  """Meta Ads mappers テスト
  2  
  3  mappers.pyの各関数にモックデータを渡して正しく変換されることを確認する。
  4  """
  5  
  6  from __future__ import annotations
  7  
  8  import pytest
  9  
 10  from mureo.meta_ads.mappers import (
 11      _cents_to_amount,
 12      _extract_conversions,
 13      _extract_cost_per_conversion,
 14      _safe_float,
 15      _safe_int,
 16      map_ad,
 17      map_ad_set,
 18      map_campaign,
 19      map_insights,
 20  )
 21  
 22  
 23  # ---------------------------------------------------------------------------
 24  # ヘルパー関数
 25  # ---------------------------------------------------------------------------
 26  
 27  
 28  @pytest.mark.unit
 29  class TestCentsToAmount:
 30      def test_文字列のセントを変換(self) -> None:
 31          assert _cents_to_amount("100000") == 1000.0
 32  
 33      def test_整数のセントを変換(self) -> None:
 34          assert _cents_to_amount(50000) == 500.0
 35  
 36      def test_Noneの場合は0(self) -> None:
 37          assert _cents_to_amount(None) == 0.0
 38  
 39      def test_ゼロの場合(self) -> None:
 40          assert _cents_to_amount("0") == 0.0
 41          assert _cents_to_amount(0) == 0.0
 42  
 43  
 44  @pytest.mark.unit
 45  class TestSafeFloat:
 46      def test_文字列を変換(self) -> None:
 47          assert _safe_float("3.14") == 3.14
 48  
 49      def test_整数を変換(self) -> None:
 50          assert _safe_float(42) == 42.0
 51  
 52      def test_Noneは0(self) -> None:
 53          assert _safe_float(None) == 0.0
 54  
 55      def test_不正文字列は0(self) -> None:
 56          assert _safe_float("abc") == 0.0
 57  
 58  
 59  @pytest.mark.unit
 60  class TestSafeInt:
 61      def test_文字列を変換(self) -> None:
 62          assert _safe_int("100") == 100
 63  
 64      def test_Noneは0(self) -> None:
 65          assert _safe_int(None) == 0
 66  
 67      def test_不正文字列は0(self) -> None:
 68          assert _safe_int("abc") == 0
 69  
 70  
 71  # ---------------------------------------------------------------------------
 72  # _extract_conversions
 73  # ---------------------------------------------------------------------------
 74  
 75  
 76  @pytest.mark.unit
 77  class TestExtractConversions:
 78      def test_コンバージョンアクションを正しく集計(self) -> None:
 79          actions = [
 80              {"action_type": "purchase", "value": "5"},
 81              {"action_type": "lead", "value": "3"},
 82              {"action_type": "link_click", "value": "100"},
 83          ]
 84          assert _extract_conversions(actions) == 8.0
 85  
 86      def test_Noneは0(self) -> None:
 87          assert _extract_conversions(None) == 0.0
 88  
 89      def test_空リストは0(self) -> None:
 90          assert _extract_conversions([]) == 0.0
 91  
 92      def test_複数のCV種別を集計(self) -> None:
 93          actions = [
 94              {"action_type": "offsite_conversion.fb_pixel_purchase", "value": "2"},
 95              {"action_type": "offsite_conversion.fb_pixel_lead", "value": "4"},
 96              {"action_type": "complete_registration", "value": "1"},
 97          ]
 98          assert _extract_conversions(actions) == 7.0
 99  
100      def test_CV以外は無視(self) -> None:
101          actions = [
102              {"action_type": "post_engagement", "value": "50"},
103              {"action_type": "video_view", "value": "200"},
104          ]
105          assert _extract_conversions(actions) == 0.0
106  
107  
108  # ---------------------------------------------------------------------------
109  # _extract_cost_per_conversion
110  # ---------------------------------------------------------------------------
111  
112  
113  @pytest.mark.unit
114  class TestExtractCostPerConversion:
115      def test_CPAを正しく抽出(self) -> None:
116          cost_per_action = [
117              {"action_type": "purchase", "value": "1500.50"},
118          ]
119          assert _extract_cost_per_conversion(cost_per_action) == 1500.50
120  
121      def test_Noneの場合(self) -> None:
122          assert _extract_cost_per_conversion(None) is None
123  
124      def test_空リスト(self) -> None:
125          assert _extract_cost_per_conversion([]) is None
126  
127      def test_該当アクション無し(self) -> None:
128          cost_per_action = [
129              {"action_type": "link_click", "value": "100"},
130          ]
131          assert _extract_cost_per_conversion(cost_per_action) is None
132  
133  
134  # ---------------------------------------------------------------------------
135  # map_campaign
136  # ---------------------------------------------------------------------------
137  
138  
139  @pytest.mark.unit
140  class TestMapCampaign:
141      def test_基本変換(self) -> None:
142          raw = {
143              "id": "campaign_123",
144              "name": "テストキャンペーン",
145              "status": "ACTIVE",
146              "objective": "CONVERSIONS",
147              "daily_budget": "500000",
148              "lifetime_budget": "0",
149              "budget_remaining": "300000",
150              "bid_strategy": "LOWEST_COST_WITH_BID_CAP",
151              "special_ad_categories": [],
152              "created_time": "2024-01-01T00:00:00",
153              "updated_time": "2024-06-01T00:00:00",
154              "start_time": "2024-01-01T00:00:00",
155              "stop_time": "",
156          }
157  
158          result = map_campaign(raw)
159  
160          assert result["campaign_id"] == "campaign_123"
161          assert result["campaign_name"] == "テストキャンペーン"
162          assert result["status"] == "ACTIVE"
163          assert result["objective"] == "CONVERSIONS"
164          assert result["daily_budget"] == 5000.0
165          assert result["budget_remaining"] == 3000.0
166  
167      def test_空の辞書(self) -> None:
168          result = map_campaign({})
169  
170          assert result["campaign_id"] == ""
171          assert result["campaign_name"] == ""
172          assert result["daily_budget"] == 0.0
173  
174  
175  # ---------------------------------------------------------------------------
176  # map_ad_set
177  # ---------------------------------------------------------------------------
178  
179  
180  @pytest.mark.unit
181  class TestMapAdSet:
182      def test_基本変換(self) -> None:
183          raw = {
184              "id": "adset_456",
185              "name": "テスト広告セット",
186              "status": "ACTIVE",
187              "campaign_id": "campaign_123",
188              "daily_budget": "200000",
189              "lifetime_budget": "0",
190              "billing_event": "IMPRESSIONS",
191              "optimization_goal": "REACH",
192              "targeting": {"age_min": 25, "age_max": 55},
193              "bid_amount": "5000",
194              "created_time": "2024-01-01T00:00:00",
195              "updated_time": "2024-06-01T00:00:00",
196              "start_time": "2024-01-01T00:00:00",
197              "end_time": "",
198          }
199  
200          result = map_ad_set(raw)
201  
202          assert result["ad_set_id"] == "adset_456"
203          assert result["ad_set_name"] == "テスト広告セット"
204          assert result["daily_budget"] == 2000.0
205          assert result["bid_amount"] == 50.0
206          assert result["targeting"] == {"age_min": 25, "age_max": 55}
207  
208      def test_空の辞書(self) -> None:
209          result = map_ad_set({})
210  
211          assert result["ad_set_id"] == ""
212          assert result["daily_budget"] == 0.0
213  
214  
215  # ---------------------------------------------------------------------------
216  # map_ad
217  # ---------------------------------------------------------------------------
218  
219  
220  @pytest.mark.unit
221  class TestMapAd:
222      def test_基本変換(self) -> None:
223          raw = {
224              "id": "ad_789",
225              "name": "テスト広告",
226              "status": "ACTIVE",
227              "adset_id": "adset_456",
228              "campaign_id": "campaign_123",
229              "creative": {"id": "creative_001", "name": "クリエイティブ1"},
230              "created_time": "2024-01-01T00:00:00",
231              "updated_time": "2024-06-01T00:00:00",
232          }
233  
234          result = map_ad(raw)
235  
236          assert result["ad_id"] == "ad_789"
237          assert result["ad_name"] == "テスト広告"
238          assert result["status"] == "ACTIVE"
239          assert result["creative_id"] == "creative_001"
240  
241      def test_creative無し(self) -> None:
242          raw = {
243              "id": "ad_999",
244              "name": "テスト",
245              "status": "PAUSED",
246          }
247  
248          result = map_ad(raw)
249  
250          assert result["creative_id"] == ""
251          assert result["creative_name"] == ""
252  
253  
254  # ---------------------------------------------------------------------------
255  # map_insights
256  # ---------------------------------------------------------------------------
257  
258  
259  @pytest.mark.unit
260  class TestMapInsights:
261      def test_基本変換(self) -> None:
262          raw = {
263              "campaign_id": "campaign_123",
264              "campaign_name": "テスト",
265              "adset_id": "",
266              "adset_name": "",
267              "ad_id": "",
268              "ad_name": "",
269              "impressions": "10000",
270              "clicks": "500",
271              "spend": "15000.50",
272              "cpc": "30.001",
273              "cpm": "1500.05",
274              "ctr": "5.0",
275              "reach": "8000",
276              "frequency": "1.25",
277              "actions": [
278                  {"action_type": "purchase", "value": "3"},
279                  {"action_type": "lead", "value": "2"},
280              ],
281              "cost_per_action_type": [
282                  {"action_type": "purchase", "value": "5000.17"},
283              ],
284          }
285  
286          result = map_insights(raw)
287  
288          assert result["impressions"] == 10000
289          assert result["clicks"] == 500
290          assert result["spend"] == 15000.50
291          assert result["conversions"] == 5.0
292          assert result["cpa"] == 5000.17
293  
294      def test_ブレイクダウンフィールド(self) -> None:
295          raw = {
296              "impressions": "100",
297              "clicks": "10",
298              "spend": "500",
299              "cpc": "50",
300              "cpm": "5000",
301              "ctr": "10",
302              "reach": "80",
303              "frequency": "1.2",
304              "age": "25-34",
305              "gender": "male",
306          }
307  
308          result = map_insights(raw)
309  
310          assert result["age"] == "25-34"
311          assert result["gender"] == "male"
312          assert "country" not in result
313  
314      def test_actionsなし(self) -> None:
315          raw = {
316              "impressions": "100",
317              "clicks": "10",
318              "spend": "500",
319              "cpc": "50",
320              "cpm": "5000",
321              "ctr": "10",
322              "reach": "80",
323              "frequency": "1.2",
324          }
325  
326          result = map_insights(raw)
327  
328          assert result["conversions"] == 0.0
329          assert result["cpa"] is None