Skip to content

Commit c9823d9

Browse files
authored
test: make _AsyncJob tests mock at a lower layer (#340)
This is intented to make the `_AsyncJob` tests more robust to changes in retry behavior. It also more explicitly tests the retry behavior by observing API calls rather than calls to certain methods.
1 parent fd08255 commit c9823d9

File tree

3 files changed

+95
-34
lines changed

3 files changed

+95
-34
lines changed

google/cloud/bigquery/client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,6 @@ def create_table(self, table, exists_ok=False, retry=DEFAULT_RETRY, timeout=None
625625
def _call_api(
626626
self, retry, span_name=None, span_attributes=None, job_ref=None, **kwargs
627627
):
628-
629628
call = functools.partial(self._connection.api_request, **kwargs)
630629
if retry:
631630
call = retry(call)

google/cloud/bigquery/job.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -529,9 +529,8 @@ def state(self):
529529
Optional[str]:
530530
the state (None until set from the server).
531531
"""
532-
status = self._properties.get("status")
533-
if status is not None:
534-
return status.get("state")
532+
status = self._properties.get("status", {})
533+
return status.get("state")
535534

536535
def _set_properties(self, api_response):
537536
"""Update properties from resource in body of ``api_response``
@@ -588,7 +587,7 @@ def _check_resource_config(cls, resource):
588587

589588
def to_api_repr(self):
590589
"""Generate a resource for the job."""
591-
raise NotImplementedError("Abstract")
590+
return copy.deepcopy(self._properties)
592591

593592
_build_resource = to_api_repr # backward-compatibility alias
594593

tests/unit/test_job.py

Lines changed: 92 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import warnings
2121

2222
import freezegun
23+
from google.api_core import exceptions
24+
import google.api_core.retry
2325
import mock
2426
import pytest
2527
import requests
@@ -70,6 +72,12 @@ def _make_connection(*responses):
7072
return mock_conn
7173

7274

75+
def _make_retriable_exception():
76+
return exceptions.TooManyRequests(
77+
"retriable exception", errors=[{"reason": "rateLimitExceeded"}]
78+
)
79+
80+
7381
def _make_job_resource(
7482
creation_time_ms=1437767599006,
7583
started_time_ms=1437767600007,
@@ -84,6 +92,7 @@ def _make_job_resource(
8492
user_email="[email protected]",
8593
):
8694
resource = {
95+
"status": {"state": "PENDING"},
8796
"configuration": {job_type: {}},
8897
"statistics": {"creationTime": creation_time_ms, job_type: {}},
8998
"etag": etag,
@@ -97,9 +106,11 @@ def _make_job_resource(
97106

98107
if started or ended:
99108
resource["statistics"]["startTime"] = started_time_ms
109+
resource["status"]["state"] = "RUNNING"
100110

101111
if ended:
102112
resource["statistics"]["endTime"] = ended_time_ms
113+
resource["status"]["state"] = "DONE"
103114

104115
if job_type == "query":
105116
resource["configuration"]["query"]["destinationTable"] = {
@@ -555,14 +566,14 @@ def test__check_resource_config_ok(self):
555566
def test__build_resource(self):
556567
client = _make_client(project=self.PROJECT)
557568
job = self._make_one(self.JOB_ID, client)
558-
with self.assertRaises(NotImplementedError):
559-
job._build_resource()
569+
resource = job._build_resource()
570+
assert resource["jobReference"]["jobId"] == self.JOB_ID
560571

561572
def test_to_api_repr(self):
562573
client = _make_client(project=self.PROJECT)
563574
job = self._make_one(self.JOB_ID, client)
564-
with self.assertRaises(NotImplementedError):
565-
job.to_api_repr()
575+
resource = job.to_api_repr()
576+
assert resource["jobReference"]["jobId"] == self.JOB_ID
566577

567578
def test__begin_already(self):
568579
job = self._set_properties_job()
@@ -965,43 +976,95 @@ def test_done_already(self):
965976

966977
self.assertTrue(job.done())
967978

968-
@mock.patch("google.api_core.future.polling.PollingFuture.result")
969-
def test_result_default_wo_state(self, result):
970-
from google.cloud.bigquery.retry import DEFAULT_RETRY
971-
972-
client = _make_client(project=self.PROJECT)
979+
def test_result_default_wo_state(self):
980+
begun_job_resource = _make_job_resource(
981+
job_id=self.JOB_ID, project_id=self.PROJECT, started=True
982+
)
983+
done_job_resource = _make_job_resource(
984+
job_id=self.JOB_ID, project_id=self.PROJECT, started=True, ended=True
985+
)
986+
conn = _make_connection(
987+
_make_retriable_exception(),
988+
begun_job_resource,
989+
_make_retriable_exception(),
990+
done_job_resource,
991+
)
992+
client = _make_client(project=self.PROJECT, connection=conn)
973993
job = self._make_one(self.JOB_ID, client)
974-
begin = job._begin = mock.Mock()
975994

976-
self.assertIs(job.result(), result.return_value)
995+
self.assertIs(job.result(), job)
977996

978-
begin.assert_called_once_with(retry=DEFAULT_RETRY, timeout=None)
979-
result.assert_called_once_with(timeout=None)
997+
begin_call = mock.call(
998+
method="POST",
999+
path=f"/projects/{self.PROJECT}/jobs",
1000+
data={"jobReference": {"jobId": self.JOB_ID, "projectId": self.PROJECT}},
1001+
timeout=None,
1002+
)
1003+
reload_call = mock.call(
1004+
method="GET",
1005+
path=f"/projects/{self.PROJECT}/jobs/{self.JOB_ID}",
1006+
query_params={},
1007+
timeout=None,
1008+
)
1009+
conn.api_request.assert_has_calls(
1010+
[begin_call, begin_call, reload_call, reload_call]
1011+
)
9801012

981-
@mock.patch("google.api_core.future.polling.PollingFuture.result")
982-
def test_result_w_retry_wo_state(self, result):
983-
client = _make_client(project=self.PROJECT)
1013+
def test_result_w_retry_wo_state(self):
1014+
begun_job_resource = _make_job_resource(
1015+
job_id=self.JOB_ID, project_id=self.PROJECT, started=True
1016+
)
1017+
done_job_resource = _make_job_resource(
1018+
job_id=self.JOB_ID, project_id=self.PROJECT, started=True, ended=True
1019+
)
1020+
conn = _make_connection(
1021+
exceptions.NotFound("not normally retriable"),
1022+
begun_job_resource,
1023+
# The call to done() / reload() does not get the custom retry
1024+
# policy passed to it, so we don't throw a non-retriable
1025+
# exception here. See:
1026+
# https://github.com/googleapis/python-bigquery/issues/24
1027+
_make_retriable_exception(),
1028+
done_job_resource,
1029+
)
1030+
client = _make_client(project=self.PROJECT, connection=conn)
9841031
job = self._make_one(self.JOB_ID, client)
985-
begin = job._begin = mock.Mock()
986-
retry = mock.Mock()
1032+
custom_predicate = mock.Mock()
1033+
custom_predicate.return_value = True
1034+
custom_retry = google.api_core.retry.Retry(predicate=custom_predicate)
9871035

988-
self.assertIs(job.result(retry=retry), result.return_value)
1036+
self.assertIs(job.result(retry=custom_retry), job)
9891037

990-
begin.assert_called_once_with(retry=retry, timeout=None)
991-
result.assert_called_once_with(timeout=None)
1038+
begin_call = mock.call(
1039+
method="POST",
1040+
path=f"/projects/{self.PROJECT}/jobs",
1041+
data={"jobReference": {"jobId": self.JOB_ID, "projectId": self.PROJECT}},
1042+
timeout=None,
1043+
)
1044+
reload_call = mock.call(
1045+
method="GET",
1046+
path=f"/projects/{self.PROJECT}/jobs/{self.JOB_ID}",
1047+
query_params={},
1048+
timeout=None,
1049+
)
1050+
conn.api_request.assert_has_calls(
1051+
[begin_call, begin_call, reload_call, reload_call]
1052+
)
9921053

993-
@mock.patch("google.api_core.future.polling.PollingFuture.result")
994-
def test_result_explicit_w_state(self, result):
995-
client = _make_client(project=self.PROJECT)
1054+
def test_result_explicit_w_state(self):
1055+
conn = _make_connection()
1056+
client = _make_client(project=self.PROJECT, connection=conn)
9961057
job = self._make_one(self.JOB_ID, client)
997-
job._properties["status"] = {"state": "DONE"}
998-
begin = job._begin = mock.Mock()
1058+
# Use _set_properties() instead of directly modifying _properties so
1059+
# that the result state is set properly.
1060+
job_resource = job._properties
1061+
job_resource["status"] = {"state": "DONE"}
1062+
job._set_properties(job_resource)
9991063
timeout = 1
10001064

1001-
self.assertIs(job.result(timeout=timeout), result.return_value)
1065+
self.assertIs(job.result(timeout=timeout), job)
10021066

1003-
begin.assert_not_called()
1004-
result.assert_called_once_with(timeout=timeout)
1067+
conn.api_request.assert_not_called()
10051068

10061069
def test_cancelled_wo_error_result(self):
10071070
client = _make_client(project=self.PROJECT)

0 commit comments

Comments
 (0)