8000 test: add mockserver tests (#928) · googleapis/python-spanner-django@517fbe9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 517fbe9

Browse files
test: add mockserver tests (#928)
* test: add mockserver tests * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 89309cd commit 517fbe9

File tree

11 files changed

+2801
-3
lines changed

11 files changed

+2801
-3
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
on:
2+
push:
3+
branches:
4+
- main
5+
pull_request:
6+
name: Run Django Spanner mockserver tests
7+
jobs:
8+
mockserver-tests:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout code
13+
uses: actions/checkout@v4
14+
- name: Set up Python 3.12
15+
uses: actions/setup-python@v5
16+
with:
17+
python-version: "3.12"
18+
- name: Install nox
19+
run: python -m pip install nox
20+
- name: Run nox
21+
run: nox -s mockserver

django_spanner/base.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,11 @@ def instance(self):
120120
:rtype: :class:`~google.cloud.spanner_v1.instance.Instance`
121121
:returns: A new instance owned by the existing Spanner Client.
122122
"""
123-
return spanner.Client(
124-
project=os.environ["GOOGLE_CLOUD_PROJECT"]
125-
).instance(self.settings_dict["INSTANCE"])
123+
if "client" in self.settings_dict["OPTIONS"]:
124+
client = self.settings_dict["OPTIONS"]["client"]
125+
else:
126+
client = spanner.Client(project=os.environ["GOOGLE_CLOUD_PROJECT"])
127+
return client.instance(self.settings_dict["INSTANCE"])
126128

127129
@property
128130
def allow_transactions_in_auto_commit(self):

noxfile.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"setup.py",
2525
]
2626

27+
MOCKSERVER_TEST_PYTHON_VERSION = "3.12"
2728
DEFAULT_PYTHON_VERSION = "3.8"
2829
SYSTEM_TEST_PYTHON_VERSIONS = ["3.8"]
2930
UNIT_TEST_PYTHON_VERSIONS = ["3.8", "3.9", "3.10"]
@@ -69,6 +70,7 @@ def lint_setup_py(session):
6970
def default(session, django_version="3.2"):
7071
# Install all test dependencies, then install this package in-place.
7172
session.install(
73+
"setuptools",
7274
"django~={}".format(django_version),
7375
"mock",
7476
"mock-import",
@@ -107,6 +109,32 @@ def unit(session):
107109
default(session, django_version="4.2")
108110

109111

112+
@nox.session(python=MOCKSERVER_TEST_PYTHON_VERSION)
113+
def mockserver(session):
114+
# Install all test dependencies, then install this package in-place.
115+
session.install(
116+
"setuptools",
117+
"django~=4.2",
118+
"mock",
119+
"mock-import",
120+
"pytest",
121+
"pytest-cov",
122+
"coverage",
123+
"sqlparse>=0.4.4",
124+
"google-cloud-spanner>=3.55.0",
125+
"opentelemetry-api==1.1.0",
126+
"opentelemetry-sdk==1.1.0",
127+
"opentelemetry-instrumentation==0.20b0",
128+
)
129+
session.install("-e", ".")
130+
session.run(
131+
"py.test",
132+
"--quiet",
133+
os.path.join("tests", "mockserver_tests"),
134+
*session.posargs,
135+
)
136+
137+
110138
def system_test(session, django_version="3.2"):
111139
"""Run the system test suite."""
112140
constraints_path = str(

tests/mockserver_tests/__init__.py

Whitespace-only changes.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2025 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from google.protobuf import empty_pb2
16+
import tests.mockserver_tests.spanner_database_admin_pb2_grpc as database_admin_grpc
17+
from google.longrunning import operations_pb2 as operations_pb2
18+
19+
20+
# An in-memory mock DatabaseAdmin server that can be used for testing.
21+
class DatabaseAdminServicer(database_admin_grpc.DatabaseAdminServicer):
22+
def __init__(self):
23+
self._requests = []
24+
25+
@property
26+
def requests(self):
27+
return self._requests
28+
29+
def clear_requests(self):
30+
self._requests = []
31+
32+
def UpdateDatabaseDdl(self, request, context):
33+
self._requests.append(request)
34+
operation = operations_pb2.Operation()
35+
operation.done = True
36+
operation.name = "projects/test-project/operations/test-operation"
37+
operation.response.Pack(empty_pb2.Empty())
38+
return operation
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Copyright 2025 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import unittest
17+
18+
from django.db import connection
19+
from google.cloud.spanner_dbapi.parsed_statement import AutocommitDmlMode
20+
import google.cloud.spanner_v1.types.type as spanner_type
21+
import google.cloud.spanner_v1.types.result_set as result_set
22+
from google.api_core.client_options import ClientOptions
23+
from google.auth.credentials import AnonymousCredentials
24+
from google.cloud.spanner_v1 import (
25+
Client,
26+
ResultSet,
27+
PingingPool,
28+
TypeCode,
29+
)
30+
from google.cloud.spanner_v1.database import Database
31+
from google.cloud.spanner_v1.instance import Instance
32+
import grpc
33+
34+
# TODO: Replace this with the mock server in the Spanner client lib
35+
from tests.mockserver_tests.mock_spanner import (
36+
SpannerServicer,
37+
start_mock_server,
38+
)
39+
from tests.mockserver_tests.mock_database_admin import DatabaseAdminServicer
40+
41+
42+
def add_result(sql: str, result: ResultSet):
43+
MockServerTestBase.spanner_service.mock_spanner.add_result(sql, result)
44+
45+
46+
def add_update_count(
47+
sql: str,
48+
count: int,
49+
dml_mode: AutocommitDmlMode = AutocommitDmlMode.TRANSACTIONAL,
50+
):
51+
if dml_mode == AutocommitDmlMode.PARTITIONED_NON_ATOMIC:
52+
stats = dict(row_count_lower_bound=count)
53+
else:
54+
stats = dict(row_count_exact=count)
55+
result = result_set.ResultSet(dict(stats=result_set.ResultSetStats(stats)))
56+
add_result(sql, result)
57+
58+
59+
def add_select1_result():
60+
add_single_result("select 1", "c", TypeCode.INT64, [("1",)])
61+
62+
63+
def add_single_result(
64+
sql: str, column_name: str, type_code: spanner_type.TypeCode, row
65+
):
66+
result = result_set.ResultSet(
67+
dict(
68+
metadata=result_set.ResultSetMetadata(
69+
dict(
70+
row_type=spanner_type.StructType(
71+
dict(
72+
fields=[
73+
spanner_type.StructType.Field(
74+
dict(
75+
name=column_name,
76+
type=spanner_type.Type(
77+
dict(code=type_code)
78+
),
79+
)
80+
)
81+
]
82+
)
83+
)
84+
)
85+
),
86+
)
87+
)
88+
result.rows.extend(row)
89+
MockServerTestBase.spanner_service.mock_spanner.add_result(sql, result)
90+
91+
92+
def add_singer_query_result(sql: str):
93+
result = result_set.ResultSet(
94+
dict(
95+
metadata=result_set.ResultSetMetadata(
96+
dict(
97+
row_type=spanner_type.StructType(
98+
dict(
99+
fields=[
100+
spanner_type.StructType.Field(
101+
dict(
102+
name="id",
103+
type=spanner_type.Type(
104+
dict(
105+
code=spanner_type.TypeCode.INT64
106+
)
107+
),
108+
)
109+
),
110+
spanner_type.StructType.Field(
111+
dict(
112+
name="first_name",
113+
type=spanner_type.Type(
114+
dict(
115+
code=spanner_type.TypeCode.STRING
116+
)
117+
),
118+
)
119+
),
120+
spanner_type.StructType.Field(
121+
dict(
122+
name="last_name",
123+
type=spanner_type.Type(
124+
dict(
125+
code=spanner_type.TypeCode.STRING
126+
)
127+
),
128+
)
129+
),
130+
]
131+
)
132+
)
133+
)
134+
),
135+
)
136+
)
137+
result.rows.extend(
138+
[
139+
(
140+
"1",
141+
"Jane",
142+
"Doe",
143+
),
144+
(
145+
"2",
146+
"John",
147+
"Doe",
148+
),
149+
]
150+
)
151+
add_result(sql, result)
152+
153+
154+
class MockServerTestBase(unittest.TestCase):
155+
server: grpc.Server = None
156+
spanner_service: SpannerServicer = None
157+
database_admin_service: DatabaseAdminServicer = None
158+
port: int = None
159+
_client = None
160+
_instance = None
161+
_database = None
162+
_pool = None
163+
164+
@classmethod
165+
def setup_class(cls):
166+
os.environ["GOOGLE_CLOUD_PROJECT"] = "mockserver-project"
167+
(
168+
MockServerTestBase.server,
169+
MockServerTestBase.spanner_service,
170+
MockServerTestBase.database_admin_service,
171+
MockServerTestBase.port,
172+
) = start_mock_server()
173+
174+
@classmethod
175+
def teardown_class(cls):
176+
if MockServerTestBase.server is not None:
177+
MockServerTestBase.server.stop(grace=None)
178+
MockServerTestBase.server = None
179+
180+
def setup_method(self, test_method):
181+
connection.settings_dict["OPTIONS"]["client"] = self.client
182+
connection.settings_dict["OPTIONS"]["pool"] = self.pool
183+
184+
def teardown_method(self, test_method):
185+
connection.close()
186+
MockServerTestBase.spanner_service.clear_requests()
187+
MockServerTestBase.database_admin_service.clear_requests()
188+
self._client = None
189+
self._instance = None
190+
self._database = None
191+
self._pool = None
192+
193+
@property
194+
def client(self) -> Client:
195+
if self._client is None:
196+
self._client = Client(
197+
project=os.environ["GOOGLE_CLOUD_PROJECT"],
198+
credentials=AnonymousCredentials(),
199+
client_options=ClientOptions(
200+
api_endpoint="localhost:" + str(MockServerTestBase.port),
201+
),
202+
)
203+
return self._client
204+
205+
@property
206+
def pool(self):
207+
if self._pool is None:
208+
self._pool = PingingPool(size=10)
209+
return self._pool
210+
211+
@property
212+
def instance(self) -> Instance:
213+
if self._instance is None:
214+
self._instance = self.client.instance("test-instance")
215+
return self._instance
216+
217+
@property
218+
def database(self) -> Database:
219+
if self._database is None:
220+
self._database = self.instance.database(
221+
"test-database", pool=PingingPool(size=10)
222+
)
223+
return self._database

0 commit comments

Comments
 (0)
0