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");
  }
});