/ tests / test_google_ads_keywords.py
test_google_ads_keywords.py
  1  """Google Ads _keywords.py テスト
  2  
  3  _KeywordsMixin の list_keywords, add_keywords, remove_keyword,
  4  pause_keyword, diagnose_keywords, suggest_keywords,
  5  list_negative_keywords, add_negative_keywords, add_negative_keywords_to_ad_group,
  6  remove_negative_keyword, get_search_terms_report のテスト。
  7  """
  8  
  9  from __future__ import annotations
 10  
 11  from typing import Any
 12  from unittest.mock import MagicMock, patch
 13  
 14  import pytest
 15  from google.ads.googleads.errors import GoogleAdsException
 16  
 17  from mureo.google_ads.client import GoogleAdsApiClient
 18  
 19  
 20  # ---------------------------------------------------------------------------
 21  # ヘルパー
 22  # ---------------------------------------------------------------------------
 23  
 24  
 25  def _make_client() -> GoogleAdsApiClient:
 26      creds = MagicMock()
 27      with patch("mureo.google_ads.client.GoogleAdsClient") as mock_gads:
 28          mock_gads.return_value = MagicMock()
 29          client = GoogleAdsApiClient(
 30              credentials=creds,
 31              customer_id="1234567890",
 32              developer_token="test-token",
 33          )
 34      return client
 35  
 36  
 37  def _make_google_ads_exception(
 38      message: str = "error",
 39      attr_name: str | None = None,
 40      error_name: str | None = None,
 41  ) -> GoogleAdsException:
 42      error = MagicMock()
 43      error.message = message
 44      if attr_name and error_name:
 45          code_attr = MagicMock()
 46          code_attr.name = error_name
 47          error.error_code = MagicMock(**{attr_name: code_attr})
 48      else:
 49          error.error_code = MagicMock(spec=[])
 50      failure = MagicMock()
 51      failure.errors = [error]
 52      exc = GoogleAdsException.__new__(GoogleAdsException)
 53      exc._failure = failure
 54      exc._call = MagicMock()
 55      exc._request_id = "req-123"
 56      type(exc).failure = property(lambda self: self._failure)
 57      return exc
 58  
 59  
 60  def _make_keyword_row(
 61      criterion_id: int = 1,
 62      text: str = "テストキーワード",
 63      match_type: int = 4,  # BROAD
 64      status: int = 2,  # ENABLED
 65      approval_status: int = 3,  # APPROVED
 66  ) -> MagicMock:
 67      row = MagicMock()
 68      row.ad_group_criterion.criterion_id = criterion_id
 69      row.ad_group_criterion.keyword.text = text
 70      row.ad_group_criterion.keyword.match_type = match_type
 71      row.ad_group_criterion.status = status
 72      row.ad_group_criterion.approval_status = approval_status
 73      row.campaign.id = 100
 74      row.campaign.name = "テストキャンペーン"
 75      row.ad_group.id = 200
 76      row.ad_group.name = "テストグループ"
 77      return row
 78  
 79  
 80  def _make_quality_keyword_row(
 81      criterion_id: int = 1,
 82      text: str = "テストKW",
 83      quality_score: int | None = 7,
 84      system_serving_status: str = "ELIGIBLE",
 85      approval_status: str = "APPROVED",
 86      creative_quality_score: str = "ABOVE_AVERAGE",
 87      post_click_quality_score: str = "ABOVE_AVERAGE",
 88      search_predicted_ctr: str = "ABOVE_AVERAGE",
 89  ) -> MagicMock:
 90      row = MagicMock()
 91      c = row.ad_group_criterion
 92      c.criterion_id = criterion_id
 93      c.keyword.text = text
 94      c.keyword.match_type = 4
 95      c.status = 2
 96      c.approval_status = approval_status
 97      c.system_serving_status = system_serving_status
 98      qi = c.quality_info
 99      qi.quality_score = quality_score
100      qi.creative_quality_score = creative_quality_score
101      qi.post_click_quality_score = post_click_quality_score
102      qi.search_predicted_ctr = search_predicted_ctr
103      row.campaign.id = 100
104      row.campaign.name = "テストキャンペーン"
105      row.ad_group.id = 200
106      row.ad_group.name = "テストグループ"
107      return row
108  
109  
110  # ---------------------------------------------------------------------------
111  # list_keywords
112  # ---------------------------------------------------------------------------
113  
114  
115  @pytest.mark.unit
116  class TestListKeywords:
117      @pytest.mark.asyncio
118      async def test_正常(self) -> None:
119          client = _make_client()
120          row = _make_keyword_row()
121          with patch.object(client, "_search", return_value=[row]):
122              result = await client.list_keywords()
123          assert len(result) == 1
124  
125      @pytest.mark.asyncio
126      async def test_campaign_idフィルタ(self) -> None:
127          client = _make_client()
128          with patch.object(client, "_search", return_value=[]) as mock_search:
129              await client.list_keywords(campaign_id="100")
130              query = mock_search.call_args[0][0]
131              assert "campaign.id = 100" in query
132  
133      @pytest.mark.asyncio
134      async def test_ad_group_idフィルタ(self) -> None:
135          client = _make_client()
136          with patch.object(client, "_search", return_value=[]) as mock_search:
137              await client.list_keywords(ad_group_id="200")
138              query = mock_search.call_args[0][0]
139              assert "adGroups/200" in query
140  
141      @pytest.mark.asyncio
142      async def test_status_filterフィルタ(self) -> None:
143          client = _make_client()
144          with patch.object(client, "_search", return_value=[]) as mock_search:
145              await client.list_keywords(status_filter="ENABLED")
146              query = mock_search.call_args[0][0]
147              assert "ad_group_criterion.status = 'ENABLED'" in query
148  
149  
150  # ---------------------------------------------------------------------------
151  # add_keywords
152  # ---------------------------------------------------------------------------
153  
154  
155  @pytest.mark.unit
156  class TestAddKeywords:
157      @pytest.mark.asyncio
158      async def test_正常(self) -> None:
159          client = _make_client()
160          mock_result1 = MagicMock()
161          mock_result1.resource_name = "customers/123/adGroupCriteria/200~1"
162          mock_result2 = MagicMock()
163          mock_result2.resource_name = "customers/123/adGroupCriteria/200~2"
164          mock_response = MagicMock()
165          mock_response.results = [mock_result1, mock_result2]
166          mock_service = MagicMock()
167          mock_service.mutate_ad_group_criteria.return_value = mock_response
168          client._client.get_service.return_value = mock_service
169          client._client.get_type.return_value = MagicMock()
170          client._client.enums = MagicMock()
171  
172          result = await client.add_keywords(
173              {
174                  "ad_group_id": "200",
175                  "keywords": [
176                      {"text": "キーワード1", "match_type": "BROAD"},
177                      {"text": "キーワード2", "match_type": "EXACT"},
178                  ],
179              }
180          )
181          assert len(result) == 2
182          assert result[0]["resource_name"] == "customers/123/adGroupCriteria/200~1"
183  
184      @pytest.mark.asyncio
185      async def test_空リスト_エラー(self) -> None:
186          client = _make_client()
187          with pytest.raises(ValueError, match="At least one keyword"):
188              await client.add_keywords(
189                  {
190                      "ad_group_id": "200",
191                      "keywords": [],
192                  }
193              )
194  
195      @pytest.mark.asyncio
196      async def test_80文字超_エラー(self) -> None:
197          client = _make_client()
198          with pytest.raises(ValueError, match="80 characters"):
199              await client.add_keywords(
200                  {
201                      "ad_group_id": "200",
202                      "keywords": [{"text": "a" * 81}],
203                  }
204              )
205  
206      @pytest.mark.asyncio
207      async def test_GoogleAdsException(self) -> None:
208          client = _make_client()
209          exc = _make_google_ads_exception("追加エラー")
210          mock_service = MagicMock()
211          mock_service.mutate_ad_group_criteria.side_effect = exc
212          client._client.get_service.return_value = mock_service
213          client._client.get_type.return_value = MagicMock()
214          client._client.enums = MagicMock()
215  
216          with pytest.raises(RuntimeError, match="error occurred"):
217              await client.add_keywords(
218                  {
219                      "ad_group_id": "200",
220                      "keywords": [{"text": "テスト"}],
221                  }
222              )
223  
224  
225  # ---------------------------------------------------------------------------
226  # remove_keyword
227  # ---------------------------------------------------------------------------
228  
229  
230  @pytest.mark.unit
231  class TestRemoveKeyword:
232      @pytest.mark.asyncio
233      async def test_正常(self) -> None:
234          client = _make_client()
235          mock_result = MagicMock()
236          mock_result.resource_name = "customers/123/adGroupCriteria/200~1"
237          mock_response = MagicMock()
238          mock_response.results = [mock_result]
239          mock_service = MagicMock()
240          mock_service.mutate_ad_group_criteria.return_value = mock_response
241          client._client.get_service.return_value = mock_service
242          client._client.get_type.return_value = MagicMock()
243  
244          result = await client.remove_keyword(
245              {
246                  "ad_group_id": "200",
247                  "criterion_id": "1",
248              }
249          )
250          assert result["resource_name"] == "customers/123/adGroupCriteria/200~1"
251  
252      @pytest.mark.asyncio
253      async def test_不正なad_group_id(self) -> None:
254          client = _make_client()
255          with pytest.raises(ValueError, match="Invalid ad_group_id"):
256              await client.remove_keyword(
257                  {
258                      "ad_group_id": "abc",
259                      "criterion_id": "1",
260                  }
261              )
262  
263      @pytest.mark.asyncio
264      async def test_不正なcriterion_id(self) -> None:
265          client = _make_client()
266          with pytest.raises(ValueError, match="Invalid criterion_id"):
267              await client.remove_keyword(
268                  {
269                      "ad_group_id": "200",
270                      "criterion_id": "abc",
271                  }
272              )
273  
274  
275  # ---------------------------------------------------------------------------
276  # pause_keyword
277  # ---------------------------------------------------------------------------
278  
279  
280  @pytest.mark.unit
281  class TestPauseKeyword:
282      @pytest.mark.asyncio
283      async def test_正常(self) -> None:
284          client = _make_client()
285          mock_result = MagicMock()
286          mock_result.resource_name = "customers/123/adGroupCriteria/200~1"
287          mock_response = MagicMock()
288          mock_response.results = [mock_result]
289          mock_service = MagicMock()
290          mock_service.mutate_ad_group_criteria.return_value = mock_response
291          client._client.get_service.return_value = mock_service
292          client._client.get_type.return_value = MagicMock()
293          client._client.enums = MagicMock()
294  
295          result = await client.pause_keyword(
296              {
297                  "ad_group_id": "200",
298                  "criterion_id": "1",
299              }
300          )
301          assert result["resource_name"] == "customers/123/adGroupCriteria/200~1"
302  
303  
304  # ---------------------------------------------------------------------------
305  # diagnose_keywords
306  # ---------------------------------------------------------------------------
307  
308  
309  @pytest.mark.unit
310  class TestDiagnoseKeywords:
311      @pytest.mark.asyncio
312      async def test_正常(self) -> None:
313          client = _make_client()
314          rows = [
315              _make_quality_keyword_row(criterion_id=1, text="KW1", quality_score=8),
316              _make_quality_keyword_row(criterion_id=2, text="KW2", quality_score=3),
317              _make_quality_keyword_row(criterion_id=3, text="KW3", quality_score=None),
318          ]
319  
320          with patch.object(client, "_search", return_value=rows):
321              result = await client.diagnose_keywords("100")
322  
323          assert result["campaign_id"] == "100"
324          assert result["total_keywords"] == 3
325          dist = result["quality_score_distribution"]
326          assert dist["high_7_10"] >= 1
327          assert dist["low_1_4"] >= 1
328          assert dist["no_score"] >= 1
329  
330      @pytest.mark.asyncio
331      async def test_空結果(self) -> None:
332          client = _make_client()
333          with patch.object(client, "_search", return_value=[]):
334              result = await client.diagnose_keywords("100")
335  
336          assert result["total_keywords"] == 0
337          assert result["campaign_name"] == ""
338  
339      @pytest.mark.asyncio
340      async def test_問題カテゴリ_low_quality_score(self) -> None:
341          client = _make_client()
342          row = _make_quality_keyword_row(quality_score=2)
343          with patch.object(client, "_search", return_value=[row]):
344              result = await client.diagnose_keywords("100")
345  
346          assert len(result["issues"]["low_quality_score"]) >= 1
347          assert result["total_issues"] >= 1
348          assert len(result["recommendations"]) >= 1
349  
350      @pytest.mark.asyncio
351      async def test_問題カテゴリ_rarely_served(self) -> None:
352          client = _make_client()
353          row = _make_quality_keyword_row(system_serving_status="RARELY_SERVED")
354          with patch.object(client, "_search", return_value=[row]):
355              result = await client.diagnose_keywords("100")
356  
357          assert len(result["issues"]["rarely_served"]) >= 1
358  
359      @pytest.mark.asyncio
360      async def test_問題カテゴリ_disapproved(self) -> None:
361          client = _make_client()
362          row = _make_quality_keyword_row(approval_status="DISAPPROVED")
363          with patch.object(client, "_search", return_value=[row]):
364              result = await client.diagnose_keywords("100")
365  
366          assert len(result["issues"]["disapproved"]) >= 1
367  
368      @pytest.mark.asyncio
369      async def test_問題カテゴリ_below_average_ctr(self) -> None:
370          client = _make_client()
371          row = _make_quality_keyword_row(search_predicted_ctr="BELOW_AVERAGE")
372          with patch.object(client, "_search", return_value=[row]):
373              result = await client.diagnose_keywords("100")
374  
375          assert len(result["issues"]["below_average_ctr"]) >= 1
376  
377      @pytest.mark.asyncio
378      async def test_問題カテゴリ_below_average_ad_relevance(self) -> None:
379          client = _make_client()
380          row = _make_quality_keyword_row(creative_quality_score="BELOW_AVERAGE")
381          with patch.object(client, "_search", return_value=[row]):
382              result = await client.diagnose_keywords("100")
383  
384          assert len(result["issues"]["below_average_ad_relevance"]) >= 1
385  
386      @pytest.mark.asyncio
387      async def test_問題カテゴリ_below_average_landing_page(self) -> None:
388          client = _make_client()
389          row = _make_quality_keyword_row(post_click_quality_score="BELOW_AVERAGE")
390          with patch.object(client, "_search", return_value=[row]):
391              result = await client.diagnose_keywords("100")
392  
393          assert len(result["issues"]["below_average_landing_page"]) >= 1
394  
395      @pytest.mark.asyncio
396      async def test_keywords上限50件(self) -> None:
397          client = _make_client()
398          rows = [_make_quality_keyword_row(criterion_id=i) for i in range(60)]
399          with patch.object(client, "_search", return_value=rows):
400              result = await client.diagnose_keywords("100")
401  
402          assert result["total_keywords"] == 60
403          assert len(result["keywords"]) == 50
404  
405  
406  # ---------------------------------------------------------------------------
407  # suggest_keywords
408  # ---------------------------------------------------------------------------
409  
410  
411  @pytest.mark.unit
412  class TestSuggestKeywords:
413      @pytest.mark.asyncio
414      async def test_正常(self) -> None:
415          client = _make_client()
416          idea1 = MagicMock()
417          idea1.text = "提案KW1"
418          idea1.keyword_idea_metrics.avg_monthly_searches = 1000
419          idea1.keyword_idea_metrics.competition = "MEDIUM"
420          idea2 = MagicMock()
421          idea2.text = "提案KW2"
422          idea2.keyword_idea_metrics.avg_monthly_searches = 500
423          idea2.keyword_idea_metrics.competition = "LOW"
424          mock_response = MagicMock()
425          mock_response.results = [idea1, idea2]
426          mock_service = MagicMock()
427          mock_service.generate_keyword_ideas.return_value = mock_response
428          client._client.get_service.return_value = mock_service
429          client._client.get_type.return_value = MagicMock()
430  
431          result = await client.suggest_keywords(["テスト"])
432          assert len(result) == 2
433          assert result[0]["keyword"] == "提案KW1"
434          assert result[0]["avg_monthly_searches"] == 1000
435  
436      @pytest.mark.asyncio
437      async def test_20件上限(self) -> None:
438          client = _make_client()
439          ideas = []
440          for i in range(30):
441              idea = MagicMock()
442              idea.text = f"KW{i}"
443              idea.keyword_idea_metrics.avg_monthly_searches = 100
444              idea.keyword_idea_metrics.competition = "LOW"
445              ideas.append(idea)
446          mock_response = MagicMock()
447          mock_response.results = ideas
448          mock_service = MagicMock()
449          mock_service.generate_keyword_ideas.return_value = mock_response
450          client._client.get_service.return_value = mock_service
451          client._client.get_type.return_value = MagicMock()
452  
453          result = await client.suggest_keywords(["テスト"])
454          assert len(result) == 20
455  
456      @pytest.mark.asyncio
457      async def test_DEVELOPER_TOKEN_NOT_APPROVED(self) -> None:
458          client = _make_client()
459          exc = _make_google_ads_exception(
460              attr_name="authorization_error",
461              error_name="DEVELOPER_TOKEN_NOT_APPROVED",
462          )
463          mock_service = MagicMock()
464          mock_service.generate_keyword_ideas.side_effect = exc
465          client._client.get_service.return_value = mock_service
466          client._client.get_type.return_value = MagicMock()
467  
468          with pytest.raises(ValueError, match="Basic or Standard access"):
469              await client.suggest_keywords(["テスト"])
470  
471      @pytest.mark.asyncio
472      async def test_一般的なGoogleAdsException(self) -> None:
473          client = _make_client()
474          exc = _make_google_ads_exception("一般エラー")
475          mock_service = MagicMock()
476          mock_service.generate_keyword_ideas.side_effect = exc
477          client._client.get_service.return_value = mock_service
478          client._client.get_type.return_value = MagicMock()
479  
480          with pytest.raises(RuntimeError, match="error occurred"):
481              await client.suggest_keywords(["テスト"])
482  
483  
484  # ---------------------------------------------------------------------------
485  # list_negative_keywords
486  # ---------------------------------------------------------------------------
487  
488  
489  @pytest.mark.unit
490  class TestListNegativeKeywords:
491      @pytest.mark.asyncio
492      async def test_正常(self) -> None:
493          client = _make_client()
494          row = MagicMock()
495          row.campaign_criterion.criterion_id = 1
496          row.campaign_criterion.keyword.text = "除外KW"
497          row.campaign_criterion.keyword.match_type = 4
498  
499          with patch.object(client, "_search", return_value=[row]):
500              result = await client.list_negative_keywords("100")
501          assert len(result) == 1
502  
503      @pytest.mark.asyncio
504      async def test_不正なcampaign_id(self) -> None:
505          client = _make_client()
506          with pytest.raises(ValueError, match="Invalid campaign_id"):
507              await client.list_negative_keywords("abc")
508  
509  
510  # ---------------------------------------------------------------------------
511  # add_negative_keywords
512  # ---------------------------------------------------------------------------
513  
514  
515  @pytest.mark.unit
516  class TestAddNegativeKeywords:
517      @pytest.mark.asyncio
518      async def test_正常(self) -> None:
519          client = _make_client()
520          mock_result = MagicMock()
521          mock_result.resource_name = "customers/123/campaignCriteria/100~1"
522          mock_response = MagicMock()
523          mock_response.results = [mock_result]
524          mock_service = MagicMock()
525          mock_service.mutate_campaign_criteria.return_value = mock_response
526          client._client.get_service.return_value = mock_service
527          client._client.get_type.return_value = MagicMock()
528          client._client.enums = MagicMock()
529  
530          result = await client.add_negative_keywords(
531              {
532                  "campaign_id": "100",
533                  "keywords": [{"text": "除外KW", "match_type": "EXACT"}],
534              }
535          )
536          assert len(result) == 1
537  
538      @pytest.mark.asyncio
539      async def test_GoogleAdsException(self) -> None:
540          client = _make_client()
541          exc = _make_google_ads_exception("追加エラー")
542          mock_service = MagicMock()
543          mock_service.mutate_campaign_criteria.side_effect = exc
544          client._client.get_service.return_value = mock_service
545          client._client.get_type.return_value = MagicMock()
546          client._client.enums = MagicMock()
547  
548          with pytest.raises(RuntimeError, match="error occurred"):
549              await client.add_negative_keywords(
550                  {
551                      "campaign_id": "100",
552                      "keywords": [{"text": "除外KW"}],
553                  }
554              )
555  
556  
557  # ---------------------------------------------------------------------------
558  # add_negative_keywords_to_ad_group
559  # ---------------------------------------------------------------------------
560  
561  
562  @pytest.mark.unit
563  class TestAddNegativeKeywordsToAdGroup:
564      @pytest.mark.asyncio
565      async def test_正常(self) -> None:
566          client = _make_client()
567          mock_result = MagicMock()
568          mock_result.resource_name = "customers/123/adGroupCriteria/200~1"
569          mock_response = MagicMock()
570          mock_response.results = [mock_result]
571          mock_service = MagicMock()
572          mock_service.mutate_ad_group_criteria.return_value = mock_response
573          client._client.get_service.return_value = mock_service
574          client._client.get_type.return_value = MagicMock()
575          client._client.enums = MagicMock()
576  
577          result = await client.add_negative_keywords_to_ad_group(
578              {
579                  "ad_group_id": "200",
580                  "keywords": [{"text": "除外KW", "match_type": "BROAD"}],
581              }
582          )
583          assert len(result) == 1
584  
585      @pytest.mark.asyncio
586      async def test_不正なad_group_id(self) -> None:
587          client = _make_client()
588          with pytest.raises(ValueError, match="Invalid ad_group_id"):
589              await client.add_negative_keywords_to_ad_group(
590                  {
591                      "ad_group_id": "abc",
592                      "keywords": [{"text": "除外KW"}],
593                  }
594              )
595  
596  
597  # ---------------------------------------------------------------------------
598  # remove_negative_keyword
599  # ---------------------------------------------------------------------------
600  
601  
602  @pytest.mark.unit
603  class TestRemoveNegativeKeyword:
604      @pytest.mark.asyncio
605      async def test_正常(self) -> None:
606          client = _make_client()
607          mock_result = MagicMock()
608          mock_result.resource_name = "customers/123/campaignCriteria/100~1"
609          mock_response = MagicMock()
610          mock_response.results = [mock_result]
611          mock_service = MagicMock()
612          mock_service.mutate_campaign_criteria.return_value = mock_response
613          client._client.get_service.return_value = mock_service
614          client._client.get_type.return_value = MagicMock()
615  
616          result = await client.remove_negative_keyword(
617              {
618                  "campaign_id": "100",
619                  "criterion_id": "1",
620              }
621          )
622          assert result["resource_name"] == "customers/123/campaignCriteria/100~1"
623  
624      @pytest.mark.asyncio
625      async def test_不正なcampaign_id(self) -> None:
626          client = _make_client()
627          with pytest.raises(ValueError, match="Invalid campaign_id"):
628              await client.remove_negative_keyword(
629                  {
630                      "campaign_id": "abc",
631                      "criterion_id": "1",
632                  }
633              )
634  
635      @pytest.mark.asyncio
636      async def test_不正なcriterion_id(self) -> None:
637          client = _make_client()
638          with pytest.raises(ValueError, match="Invalid criterion_id"):
639              await client.remove_negative_keyword(
640                  {
641                      "campaign_id": "100",
642                      "criterion_id": "abc",
643                  }
644              )
645  
646  
647  # ---------------------------------------------------------------------------
648  # get_search_terms_report
649  # ---------------------------------------------------------------------------
650  
651  
652  @pytest.mark.unit
653  class TestGetSearchTermsReport:
654      @pytest.mark.asyncio
655      async def test_正常(self) -> None:
656          client = _make_client()
657          row = MagicMock()
658          row.search_term_view.search_term = "テスト検索語句"
659          row.metrics.impressions = 100
660          row.metrics.clicks = 10
661          row.metrics.cost_micros = 1000_000_000
662          row.metrics.conversions = 1
663          row.metrics.ctr = 0.1
664  
665          with patch.object(client, "_search", return_value=[row]):
666              result = await client.get_search_terms_report()
667          assert len(result) == 1
668  
669      @pytest.mark.asyncio
670      async def test_campaign_idフィルタ(self) -> None:
671          client = _make_client()
672          with patch.object(client, "_search", return_value=[]) as mock_search:
673              await client.get_search_terms_report(campaign_id="100")
674              query = mock_search.call_args[0][0]
675              assert "campaign.id = 100" in query
676  
677      @pytest.mark.asyncio
678      async def test_ad_group_idフィルタ(self) -> None:
679          client = _make_client()
680          with patch.object(client, "_search", return_value=[]) as mock_search:
681              await client.get_search_terms_report(ad_group_id="200")
682              query = mock_search.call_args[0][0]
683              assert "ad_group.id = 200" in query
684  
685      @pytest.mark.asyncio
686      async def test_period指定(self) -> None:
687          client = _make_client()
688          with patch.object(client, "_search", return_value=[]) as mock_search:
689              await client.get_search_terms_report(period="LAST_7_DAYS")
690              query = mock_search.call_args[0][0]
691              assert "DURING LAST_7_DAYS" in query