Skip to content

Commit 3d891a5

Browse files
MattOatesMatt Oatesalexanderankin
authored
fix(google): add support for Datastore emulator (#508)
Expands the google module with a DatastoreContainer using the beta Datastore emulator using the same image as the PubSubContainer. Im already using a local copy of this in production. It would be nice to not have to support copy paste solutions and instead see it added to the google module. This is my first PR so please let me know what I need to do to get this over the line. Thanks. Looks like @tillahoffmann wrote the original PubSub emulator container --------- Co-authored-by: Matt Oates <[email protected]> Co-authored-by: David Ankin <[email protected]>
1 parent 8addc11 commit 3d891a5

File tree

6 files changed

+165
-4
lines changed

6 files changed

+165
-4
lines changed

modules/google/README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
.. autoclass:: testcontainers.google.DatastoreContainer
2+
.. title:: testcontainers.google.DatastoreContainer
13
.. autoclass:: testcontainers.google.PubSubContainer
24
.. title:: testcontainers.google.PubSubContainer
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from .datastore import DatastoreContainer # noqa: F401
12
from .pubsub import PubSubContainer # noqa: F401
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#
2+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3+
# not use this file except in compliance with the License. You may obtain
4+
# a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
# License for the specific language governing permissions and limitations
12+
# under the License.
13+
import os
14+
from unittest.mock import patch
15+
16+
from google.cloud import datastore
17+
from testcontainers.core.container import DockerContainer
18+
from testcontainers.core.waiting_utils import wait_for_logs
19+
20+
21+
class DatastoreContainer(DockerContainer):
22+
"""
23+
Datastore container for testing managed message queues.
24+
25+
Example:
26+
27+
The example will spin up a Google Cloud Datastore emulator that you can use for integration
28+
tests. The :code:`datastore` instance provides convenience methods :code:`get_datastore_client` to
29+
connect to the emulator without having to set the environment variable :code:`DATASTORE_EMULATOR_HOST`.
30+
31+
.. doctest::
32+
33+
>>> from testcontainers.google import DatastoreContainer
34+
35+
>>> config = DatastoreContainer()
36+
>>> with config as datastore:
37+
... datastore_client = datastore.get_datastore_client()
38+
"""
39+
40+
def __init__(
41+
self,
42+
image: str = "google/cloud-sdk:emulators",
43+
project: str = "test-project",
44+
port: int = 8081,
45+
**kwargs,
46+
) -> None:
47+
super().__init__(image=image, **kwargs)
48+
self.project = project
49+
self.port = port
50+
self.with_exposed_ports(self.port)
51+
self.with_command(
52+
f"gcloud beta emulators datastore start --no-store-on-disk --project={project} --host-port=0.0.0.0:{port}"
53+
)
54+
55+
def get_datastore_emulator_host(self) -> str:
56+
return f"{self.get_container_host_ip()}:{self.get_exposed_port(self.port)}"
57+
58+
def get_datastore_client(self, **kwargs) -> datastore.Client:
59+
wait_for_logs(self, "Dev App Server is now running.", timeout=30.0)
60+
env_vars = {
61+
"DATASTORE_DATASET": self.project,
62+
"DATASTORE_EMULATOR_HOST": self.get_datastore_emulator_host(),
63+
"DATASTORE_EMULATOR_HOST_PATH": f"{self.get_datastore_emulator_host()}/datastore",
64+
"DATASTORE_HOST": f"http://{self.get_datastore_emulator_host()}",
65+
"DATASTORE_PROJECT_ID": self.project,
66+
}
67+
with patch.dict(os.environ, env_vars):
68+
return datastore.Client(**kwargs)

modules/google/tests/test_google.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from queue import Queue
2+
from google.cloud.datastore import Entity
23

34
from testcontainers.core.waiting_utils import wait_for_logs
4-
from testcontainers.google import PubSubContainer
5+
from testcontainers.google import PubSubContainer, DatastoreContainer
56

67

78
def test_pubsub_container():
@@ -27,3 +28,49 @@ def test_pubsub_container():
2728
message = queue.get(timeout=1)
2829
assert message.data == b"Hello world!"
2930
message.ack()
31+
32+
33+
def test_datastore_container_creation():
34+
# Initialize the Datastore emulator container
35+
with DatastoreContainer() as datastore:
36+
# Obtain a datastore client configured to connect to the emulator
37+
client = datastore.get_datastore_client()
38+
39+
# Define a unique key for a test entity to ensure test isolation
40+
key = client.key("TestKind", "test_id_1")
41+
42+
# Create and insert a new entity
43+
entity = Entity(key=key)
44+
entity.update({"foo": "bar"})
45+
client.put(entity)
46+
47+
# Fetch the just-inserted entity directly
48+
fetched_entity = client.get(key)
49+
50+
# Assert that the fetched entity matches what was inserted
51+
assert fetched_entity is not None, "Entity was not found in the datastore."
52+
assert fetched_entity["foo"] == "bar", "Entity attribute 'foo' did not match expected value 'bar'."
53+
54+
55+
def test_datastore_container_isolation():
56+
# Initialize the Datastore emulator container
57+
with DatastoreContainer() as datastore:
58+
# Obtain a datastore client configured to connect to the emulator
59+
client = datastore.get_datastore_client()
60+
61+
# Define a unique key for a test entity to ensure test isolation
62+
key = client.key("TestKind", "test_id_1")
63+
64+
# Create and insert a new entity
65+
entity = Entity(key=key)
66+
entity.update({"foo": "bar"})
67+
client.put(entity)
68+
69+
# Create a second container and try to fetch the entity to makesure its a different container
70+
with DatastoreContainer() as datastore2:
71+
assert (
72+
datastore.get_datastore_emulator_host() != datastore2.get_datastore_emulator_host()
73+
), "Datastore containers use the same port."
74+
client2 = datastore2.get_datastore_client()
75+
fetched_entity2 = client2.get(key)
76+
assert fetched_entity2 is None, "Entity was found in the datastore."

poetry.lock

Lines changed: 44 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ python-arango = { version = "^7.8", optional = true }
6767
azure-storage-blob = { version = "^12.19", optional = true }
6868
clickhouse-driver = { version = "*", optional = true }
6969
google-cloud-pubsub = { version = ">=2", optional = true }
70+
google-cloud-datastore = { version = ">=2", optional = true }
7071
influxdb = { version = "*", optional = true }
7172
influxdb-client = { version = "*", optional = true }
7273
kubernetes = { version = "*", optional = true }
@@ -90,7 +91,7 @@ arangodb = ["python-arango"]
9091
azurite = ["azure-storage-blob"]
9192
clickhouse = ["clickhouse-driver"]
9293
elasticsearch = []
93-
google = ["google-cloud-pubsub"]
94+
google = ["google-cloud-pubsub", "google-cloud-datastore"]
9495
influxdb = ["influxdb", "influxdb-client"]
9596
k3s = ["kubernetes", "pyyaml"]
9697
kafka = []

0 commit comments

Comments
 (0)