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