GCloud Module
Testcontainers module for the Google Cloud Platform's Cloud SDK.
Install
npm install @testcontainers/gcloud --save-dev
The module now supports multiple emulators, including firestore
, which offers both native
and datastore
modes.
To utilize these emulators, you should employ the following classes:
Mode | Class | Container Image |
---|---|---|
Firestore Native mode | FirestoreEmulatorContainer | gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators |
Firestore Datastore mode | DatastoreEmulatorContainer | gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators |
Cloud PubSub mode | PubSubEmulatorContainer | gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators |
Cloud Storage mode | CloudStorageEmulatorContainer | https://hub.docker.com/r/fsouza/fake-gcs-server |
Examples
Firestore Native mode
it("should work using default version", async () => {
const firestoreEmulatorContainer = await new FirestoreEmulatorContainer().start();
await checkFirestore(firestoreEmulatorContainer);
await firestoreEmulatorContainer.stop();
});
it("should work using version 468.0.0", async () => {
const firestoreEmulatorContainer = await new FirestoreEmulatorContainer(
"gcr.io/google.com/cloudsdktool/google-cloud-cli:468.0.0-emulators"
).start();
await checkFirestore(firestoreEmulatorContainer);
await firestoreEmulatorContainer.stop();
});
Firestore Datastore mode
it("should work using default version", async () => {
const datastoreEmulatorContainer = await new DatastoreEmulatorContainer().start();
await checkDatastore(datastoreEmulatorContainer);
await datastoreEmulatorContainer.stop();
});
it("should work using version 468.0.0", async () => {
const datastoreEmulatorContainer = await new DatastoreEmulatorContainer(
"gcr.io/google.com/cloudsdktool/google-cloud-cli:468.0.0-emulators"
).start();
await checkDatastore(datastoreEmulatorContainer);
await datastoreEmulatorContainer.stop();
});
Cloud PubSub mode
import { PubSubEmulatorContainer, StartedPubSubEmulatorContainer } from "./pubsub-emulator-container";
import { PubSub } from "@google-cloud/pubsub";
describe("PubSubEmulatorContainer", () => {
jest.setTimeout(240_000);
it("should work using default version", async () => {
const pubsubEmulatorContainer = await new PubSubEmulatorContainer().start();
await checkPubSub(pubsubEmulatorContainer);
await pubsubEmulatorContainer.stop();
});
async function checkPubSub(pubsubEmulatorContainer: StartedPubSubEmulatorContainer) {
expect(pubsubEmulatorContainer).toBeDefined();
const pubSubClient = new PubSub({
projectId: "test-project",
apiEndpoint: pubsubEmulatorContainer.getEmulatorEndpoint(),
});
expect(pubSubClient).toBeDefined();
const [createdTopic] = await pubSubClient.createTopic("test-topic");
expect(createdTopic).toBeDefined();
// Note: topic name format is projects/<projectId>/topics/<topicName>
expect(createdTopic.name).toContain("test-topic");
}
});
Cloud Storage mode
The Cloud Storage mode doesn't rely on a built-in emulator created by Google but instead depends on a fake Cloud Storage server implemented by Francisco Souza. The project is open-source, and the repository can be found at fsouza/fake-gcs-server.
import { CloudStorageEmulatorContainer, StartedCloudStorageEmulatorContainer } from "./cloudstorage-emulator-container";
import { Storage } from "@google-cloud/storage";
import { ReadableStream } from "node:stream/web";
import { setupServer } from "msw/node";
async function getRequestBodyFromReadableStream(stream: ReadableStream<Uint8Array>): Promise<string> {
const decoder = new TextDecoder();
const reader = stream.getReader();
let fullString = "";
try {
// eslint-disable-next-line no-constant-condition
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (value) {
fullString += decoder.decode(value, { stream: true });
}
}
fullString += decoder.decode();
} finally {
reader.releaseLock();
}
return fullString;
}
describe("CloudStorageEmulatorContainer", () => {
jest.setTimeout(240_000);
const server = setupServer();
beforeAll(() => {
server.listen({
onUnhandledRequest: "bypass",
});
});
beforeEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});
it("should work using default version", async () => {
const cloudstorageEmulatorContainer = await new CloudStorageEmulatorContainer().start();
await checkCloudStorage(cloudstorageEmulatorContainer);
await cloudstorageEmulatorContainer.stop();
});
it("should use the provided external URL", async () => {
const cloudstorageEmulatorContainer = await new CloudStorageEmulatorContainer()
.withExternalURL("http://cdn.company.local")
.start();
expect(cloudstorageEmulatorContainer).toBeDefined();
expect(cloudstorageEmulatorContainer.getExternalUrl()).toBe("http://cdn.company.local");
await cloudstorageEmulatorContainer.stop();
});
it("should be able update the external URL of running instance", async () => {
const cloudstorageEmulatorContainer = await new CloudStorageEmulatorContainer()
.withExternalURL("http://cdn.company.local")
.start();
expect(cloudstorageEmulatorContainer).toBeDefined();
expect(cloudstorageEmulatorContainer.getExternalUrl()).toBe("http://cdn.company.local");
const executedRequests: Request[] = [];
// Observe the outgoing request to change the configuration
server.events.on("request:start", ({ request }) => {
if (request.url.includes("/_internal/config")) {
const clonedRequest = request.clone();
executedRequests.push(clonedRequest);
}
});
await cloudstorageEmulatorContainer.updateExternalUrl("http://files.company.local");
expect(executedRequests).toHaveLength(1);
const [requestInfo] = executedRequests;
const expectedRequestUrl = cloudstorageEmulatorContainer.getEmulatorEndpoint() + "/_internal/config";
expect(requestInfo.url).toContain(expectedRequestUrl);
expect(requestInfo.method).toBe("PUT");
const requestBody = await getRequestBodyFromReadableStream(requestInfo.body as ReadableStream<Uint8Array>);
expect(requestBody).toBeDefined();
const requestBodyAsJson = JSON.parse(requestBody);
expect(requestBodyAsJson).toEqual(expect.objectContaining({ externalUrl: "http://files.company.local" }));
expect(cloudstorageEmulatorContainer.getExternalUrl()).toBe("http://files.company.local");
await cloudstorageEmulatorContainer.stop();
});
it("should use emulator endpoint as default external URL", async () => {
let configUpdated = false;
server.events.on("request:start", ({ request }) => {
if (request.url.includes("/_internal/config")) configUpdated = true;
});
const container = await new CloudStorageEmulatorContainer().start();
expect(configUpdated).toBe(true);
expect(container.getExternalUrl()).toBe(container.getEmulatorEndpoint());
expect((await fetch(`${container.getExternalUrl()}/_internal/healthcheck`)).status).toBe(200);
await container.stop();
});
it("should allow skipping updating the external URL automatically", async () => {
let configUpdated = false;
server.events.on("request:start", ({ request }) => {
if (request.url.includes("/_internal/config")) configUpdated = true;
});
const container = await new CloudStorageEmulatorContainer().withAutoUpdateExternalUrl(false).start();
expect(configUpdated).toBe(false);
expect(container.getExternalUrl()).toBe(undefined);
expect((await fetch(`${container.getEmulatorEndpoint()}/_internal/healthcheck`)).status).toBe(200);
await container.stop();
});
async function checkCloudStorage(cloudstorageEmulatorContainer: StartedCloudStorageEmulatorContainer) {
expect(cloudstorageEmulatorContainer).toBeDefined();
const cloudStorageClient = new Storage({
projectId: "test-project",
apiEndpoint: cloudstorageEmulatorContainer.getExternalUrl(),
});
expect(cloudStorageClient).toBeDefined();
const createdBucket = await cloudStorageClient.createBucket("test-bucket");
expect(createdBucket).toBeDefined();
const [buckets] = await cloudStorageClient.getBuckets();
expect(buckets).toBeDefined();
expect(buckets).toHaveLength(1);
const [firstBucket] = buckets;
expect(firstBucket.name).toBe("test-bucket");
}
});