Discover how to build your own, offline "LocalStack" for Google APIs using WireMock and TestContainers. In this post, I will show you how to set up a two-purpose mocking strategy. By writing your JSON mock responses just once, you can reuse them to power automated integration tests and spin up a standalone mock server for local (UI) development. No more production data and sharing of google keys between developers, no more 403 User Rate Limit Exceeded.

In my previous post, I wrote about building internal tools using an AI web client as a sparring partner. When I initially set out to build a Google Workspace integration, with a backend API and an HTMX web dashboard, I faced a classic development challenge: how do I properly test this without constantly hitting the live Google APIs? And how can other developers contribute without the need of our production keys?

I wanted to mock the API for my automated integration tests, but I also wanted to spin up the local UI and click around without triggering real network calls. For AWS developers, LocalStack is the go-to solution for offline testing. For Google Workspace, there is currently a new service in public preview called local.cloud. However, at that time it didn’t exist, and moreover I wanted a free, self-hosted alternative that gave me full control.

I ended up building my own "LocalStack" using WireMock and TestContainers. This setup paid off really well. Later on, when the application’s scope expanded to audit deep folder permissions via the Google Drive API, I was able to use this exact same mock setup to safely simulate rate limit errors and properly test my exponential backoff logic.

localstack for google apis

Here is how you can set this up for your own projects.

Step 1: The Shared Mock Payloads (JSON)

The foundation of this approach is separating your mock definitions from your Java code. I want to store the payloads exactly once, and share them between our Java Integration Tests (during the test phase) and our local standalone Docker environment (during local development).

To make everything play nicely with both Spring’s test classpath and Docker Compose, I split the response payloads (__files) from the standalone routing definitions (mappings):

.
├── src/
│   └── test/
│       └── resources/
│           └── __files/
│               ├── google_list_users.json
│               ├── google_drive_rate_limit.json
│               └── google_drive_permissions.json
└── wiremock/
    └── mappings/
        ├── get_directory_users.json
        └── get_drive_permissions.json

You only have to copy-paste the raw API responses into the __files directory once. For example:

src/test/resources/__files/google_list_users.json:

{
  "kind": "admin#directory#users",
  "users": [
    {
      "id": "user-id-1",
      "primaryEmail": "sullivan@monstersinc.com",
      "name": {
        "givenName": "James",
        "familyName": "Sullivan",
        "fullName": "James P. Sullivan"
      },
      "orgUnitPath": "/Scare Floor F"
    }
  ]
}

Step 2: Automated Integration Tests

For our integration tests, I use the WireMock Java Client to programmatically route requests to our shared JSON payloads. This is a best practice, as it keeps the test scenario isolated within the test method.

First, add the TestContainers WireMock module to your pom.xml:

<dependency>
    <groupId>org.wiremock.integrations.testcontainers</groupId>
    <artifactId>wiremock-testcontainers-module</artifactId>
    <version>1.0-alpha-15</version>
    <scope>test</scope>
</dependency>

Next, I set up the Integration Test. There are three important tricks here: 1. I construct the WireMockContainer and map the __files from our test classpath into the container. 2. I use @DynamicPropertySource to dynamically override the Spring Boot base URL so the Google API Client points to our container. 3. I bypass the official Google SDK credential checks by mocking the AuthManager with @MockitoBean and returning an empty HttpRequestInitializer.

@SpringBootTest
@Testcontainers
class GoogleApiAdapterIT {

    @Container
    static WireMockContainer wiremockServer = new WireMockContainer("wiremock/wiremock:latest")
            .withClasspathResourceMapping("__files", "/home/wiremock/__files", BindMode.READ_ONLY);

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        // Overrides the base-url in application.yaml to point to the test container
        registry.add("google.base-url", () -> wiremockServer.getBaseUrl() + "/");
    }

    @MockitoBean
    private GoogleAuthManager authManager;

    @Autowired
    private GoogleDriveAdapter adapter;

    @BeforeEach
    void setUp() throws Exception {
        // Return an empty functionless HttpRequestInitializer for testing, skipping real OAuth
        when(authManager.getCredentialsForWorkspace(any())).thenReturn(request -> {});
    }

    @Test
    void shouldRetryOnUserRateLimitExceededAndEventuallySucceed() {
        WireMock wireMockClient = new WireMock(wiremockServer.getHost(), wiremockServer.getPort());

        // Stateful Mocking: Fail the first time, succeed on the retry!
        wireMockClient.register(
                get(urlPathEqualTo("/drive/v3/files/FILE-123/permissions"))
                        .inScenario("RateLimitScenario")
                        .whenScenarioStateIs(Scenario.STARTED)
                        .willReturn(aResponse()
                                .withStatus(403)
                                .withHeader("Content-Type", "application/json")
                                .withBodyFile("google_drive_rate_limit.json")) // Uses our shared file!
                        .willSetStateTo("RETRY_SUCCESS")
        );

        wireMockClient.register(
                get(urlPathEqualTo("/drive/v3/files/FILE-123/permissions"))
                        .inScenario("RateLimitScenario")
                        .whenScenarioStateIs("RETRY_SUCCESS")
                        .willReturn(aResponse()
                                .withStatus(200)
                                .withHeader("Content-Type", "application/json")
                                .withBodyFile("google_drive_permissions.json")) // Uses our shared file!
        );

        // Because of our stateful WireMock scenario, the adapter will hit a 403 first,
        // trigger the Spring @Retryable backoff, and succeed on the second attempt.
        List<DrivePermissionOverride> overrides = adapter.getPermissionOverrides("MONSTERS_INC", targetDrive);

        assertThat(overrides).isNotEmpty();
    }
}

Step 3: Local Standalone Server for UI and Manual Testing

With the automated tests running properly, you might want to boot up the application locally to test your frontend or manually invoke your CLI commands. Since I can’t use programmatic Java stubs outside of our tests, I use the wiremock/mappings folder to define our static routes for the standalone server.

A mapping file (wiremock/mappings/get_directory_users.json) looks like this:

{
  "request": {
    "method": "GET",
    "urlPattern": "/admin/directory/v1/users.*"
  },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "bodyFileName": "google_list_users.json"
  }
}

I can now spin up a standalone WireMock server using Docker Compose, mounting both our static mappings and the shared payloads:

services:
  google-api-mock:
    image: wiremock/wiremock:3x
    container_name: google-api-mock
    ports:
      - "8081:8080"
    volumes:
      - ./wiremock/mappings:/home/wiremock/mappings
      - ./src/test/resources/__files:/home/wiremock/__files
    command:
      - "--global-response-templating"
    restart: unless-stopped

In your application-dev.yaml, you hardcode the API base URL to point to the exposed Docker port:

google:
  base-url: http://localhost:8081/

Bypassing credentials locally If you use the official Google Java Client libraries (e.g., google-api-services-drive), the SDK expects valid OAuth2 credentials or a Service Account JSON file. If you try to run this locally without real keys, the application crashes before it even attempts to make an HTTP request to WireMock.

I use a Spring Profile (@Profile("dev")) to inject a dummy auth manager:

// Production Implementation
@Component
@Profile("!dev")
public class DefaultGoogleAuthManager implements GoogleAuthManager {
    @Override
    public HttpRequestInitializer getCredentialsForWorkspace(Workspace workspace) {
        // Loads real ServiceAccountCredentials from a JSON file
        return new HttpCredentialsAdapter(getRawCredentials(workspace));
    }
}

// Local / Testing Implementation
@Component
@Profile("dev")
public class GoogleAuthManagerDev implements GoogleAuthManager {
    @Override
    public HttpRequestInitializer getCredentialsForWorkspace(Workspace workspace) {
        // Returns an empty initializer. Bypasses real Google OAuth entirely.
        return request -> {};
    }
}

By activating the dev profile during manual testing or local development, the Google Client will happily send requests without requiring cryptographic signatures.

Now, firing up the environment is as simple as:

  1. docker compose up -d (Starts the mock API on port 8081)

  2. Run your Spring Boot application with the dev profile enabled.

Conclusion

By combining WireMock and TestContainers, you can create a robust, dual-purpose "LocalStack" tailored to your application’s needs.

Initially, it gave me reliable tests and a fast UI development loop without sharing auth keys and hitting Google’s infrastructure. Later on, when I needed to build retry-mechanisms for Google’s API rate limits, the setup was already there to simulate those edge cases. It requires some handcrafting JSON files up front, but the return on investment for your development flow is a huge help.

shadow-left