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