test_meta_ads_split_test.py
1 """Meta Ads Split Test (A/Bテスト) ユニットテスト 2 3 SplitTestMixinの全メソッドをモックベースでテストする。 4 """ 5 6 from __future__ import annotations 7 8 from unittest.mock import AsyncMock 9 10 import pytest 11 12 from mureo.meta_ads._split_test import SplitTestMixin 13 14 15 # --------------------------------------------------------------------------- 16 # ヘルパー: Mixinをテスト可能にするモッククラス 17 # --------------------------------------------------------------------------- 18 19 20 def _make_mock_client() -> SplitTestMixin: 21 """SplitTestMixinにモック _get/_post を付与したインスタンスを生成""" 22 23 class MockClient(SplitTestMixin): 24 def __init__(self) -> None: 25 self._ad_account_id = "act_123" 26 self._get = AsyncMock(return_value={"data": []}) 27 self._post = AsyncMock(return_value={"id": "study_001"}) 28 29 return MockClient() 30 31 32 # =========================================================================== 33 # SplitTestMixin テスト 34 # =========================================================================== 35 36 37 @pytest.mark.unit 38 class TestSplitTestMixin: 39 @pytest.fixture() 40 def client(self) -> SplitTestMixin: 41 return _make_mock_client() 42 43 # ----------------------------------------------------------------------- 44 # 1. test_list_split_tests 45 # ----------------------------------------------------------------------- 46 @pytest.mark.asyncio 47 async def test_list_split_tests(self, client: SplitTestMixin) -> None: 48 """スプリットテスト一覧を取得できること""" 49 client._get = AsyncMock( 50 return_value={ 51 "data": [ 52 {"id": "study_001", "name": "CPA比較テスト"}, 53 {"id": "study_002", "name": "クリエイティブテスト"}, 54 ] 55 } 56 ) 57 result = await client.list_split_tests() 58 assert len(result) == 2 59 assert result[0]["id"] == "study_001" 60 client._get.assert_called_once() 61 call_args = client._get.call_args 62 assert "/act_123/ad_studies" in call_args[0][0] 63 64 # ----------------------------------------------------------------------- 65 # 2. test_get_split_test 66 # ----------------------------------------------------------------------- 67 @pytest.mark.asyncio 68 async def test_get_split_test(self, client: SplitTestMixin) -> None: 69 """スプリットテスト詳細を取得できること""" 70 client._get = AsyncMock( 71 return_value={ 72 "id": "study_001", 73 "name": "CPA比較テスト", 74 "type": "SPLIT_TEST", 75 "start_time": "2024-01-01T00:00:00+0000", 76 "end_time": "2024-01-15T00:00:00+0000", 77 } 78 ) 79 result = await client.get_split_test("study_001") 80 assert result["id"] == "study_001" 81 assert result["type"] == "SPLIT_TEST" 82 client._get.assert_called_once() 83 call_args = client._get.call_args 84 assert "/study_001" in call_args[0][0] 85 86 # ----------------------------------------------------------------------- 87 # 3. test_create_split_test 88 # ----------------------------------------------------------------------- 89 @pytest.mark.asyncio 90 async def test_create_split_test(self, client: SplitTestMixin) -> None: 91 """スプリットテストをデフォルト信頼度(95)で作成できること""" 92 client._post = AsyncMock(return_value={"id": "study_new"}) 93 cells = [ 94 {"name": "Control", "adsets": ["adset_1"]}, 95 {"name": "Test", "adsets": ["adset_2"]}, 96 ] 97 objectives = [{"type": "COST_PER_RESULT"}] 98 result = await client.create_split_test( 99 name="CPA比較テスト", 100 cells=cells, 101 objectives=objectives, 102 start_time="2024-01-01T00:00:00+0000", 103 end_time="2024-01-15T00:00:00+0000", 104 ) 105 assert result["id"] == "study_new" 106 client._post.assert_called_once() 107 call_args = client._post.call_args 108 assert "/act_123/ad_studies" in call_args[0][0] 109 data = call_args[1].get("data") or call_args[0][1] 110 assert data["name"] == "CPA比較テスト" 111 assert data["confidence_level"] == 95 112 113 # ----------------------------------------------------------------------- 114 # 4. test_create_split_test_custom_confidence 115 # ----------------------------------------------------------------------- 116 @pytest.mark.asyncio 117 async def test_create_split_test_custom_confidence( 118 self, client: SplitTestMixin 119 ) -> None: 120 """カスタム信頼度でスプリットテストを作成できること""" 121 client._post = AsyncMock(return_value={"id": "study_custom"}) 122 cells = [ 123 {"name": "A", "adsets": ["adset_a"]}, 124 {"name": "B", "adsets": ["adset_b"]}, 125 ] 126 objectives = [{"type": "COST_PER_RESULT"}] 127 result = await client.create_split_test( 128 name="カスタム信頼度テスト", 129 cells=cells, 130 objectives=objectives, 131 start_time="2024-02-01T00:00:00+0000", 132 end_time="2024-02-15T00:00:00+0000", 133 confidence_level=90, 134 ) 135 assert result["id"] == "study_custom" 136 call_args = client._post.call_args 137 data = call_args[1].get("data") or call_args[0][1] 138 assert data["confidence_level"] == 90 139 140 # ----------------------------------------------------------------------- 141 # 5. test_end_split_test 142 # ----------------------------------------------------------------------- 143 @pytest.mark.asyncio 144 async def test_end_split_test(self, client: SplitTestMixin) -> None: 145 """スプリットテストを終了できること""" 146 client._post = AsyncMock(return_value={"success": True}) 147 result = await client.end_split_test("study_001") 148 assert result["success"] is True 149 client._post.assert_called_once() 150 call_args = client._post.call_args 151 assert "/study_001" in call_args[0][0] 152 153 # ----------------------------------------------------------------------- 154 # 6. test_list_split_tests_empty 155 # ----------------------------------------------------------------------- 156 @pytest.mark.asyncio 157 async def test_list_split_tests_empty(self, client: SplitTestMixin) -> None: 158 """スプリットテストがない場合に空リストを返すこと""" 159 client._get = AsyncMock(return_value={"data": []}) 160 result = await client.list_split_tests() 161 assert result == [] 162 163 # ----------------------------------------------------------------------- 164 # 7. test_api_error 165 # ----------------------------------------------------------------------- 166 @pytest.mark.asyncio 167 async def test_api_error(self, client: SplitTestMixin) -> None: 168 """APIエラー時にRuntimeErrorが伝播すること""" 169 client._get = AsyncMock(side_effect=RuntimeError("Meta API request failed")) 170 with pytest.raises(RuntimeError, match="Meta API"): 171 await client.list_split_tests() 172 173 # ----------------------------------------------------------------------- 174 # 8. test_create_split_test_invalid_confidence 175 # ----------------------------------------------------------------------- 176 @pytest.mark.asyncio 177 async def test_create_split_test_invalid_confidence( 178 self, client: SplitTestMixin 179 ) -> None: 180 """無効なconfidence_levelでValueErrorが発生すること""" 181 cells = [ 182 {"name": "A", "adsets": ["adset_a"]}, 183 {"name": "B", "adsets": ["adset_b"]}, 184 ] 185 objectives = [{"type": "COST_PER_RESULT"}] 186 with pytest.raises(ValueError, match="confidence_level"): 187 await client.create_split_test( 188 name="無効テスト", 189 cells=cells, 190 objectives=objectives, 191 start_time="2024-01-01T00:00:00+0000", 192 end_time="2024-01-15T00:00:00+0000", 193 confidence_level=99, 194 ) 195 196 # ----------------------------------------------------------------------- 197 # 9. test_get_split_test_with_results 198 # ----------------------------------------------------------------------- 199 @pytest.mark.asyncio 200 async def test_get_split_test_with_results(self, client: SplitTestMixin) -> None: 201 """結果付きスプリットテスト詳細を取得できること""" 202 client._get = AsyncMock( 203 return_value={ 204 "id": "study_001", 205 "name": "CPA比較テスト", 206 "type": "SPLIT_TEST", 207 "results": [ 208 {"cell_id": "cell_1", "winner": True}, 209 {"cell_id": "cell_2", "winner": False}, 210 ], 211 } 212 ) 213 result = await client.get_split_test("study_001") 214 assert "results" in result 215 assert len(result["results"]) == 2