Skip to content

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