Cloud Run Testing Guide: Unit, Integration, and Load Testing for GCP Serverless

Cloud Run Testing Guide: Unit, Integration, and Load Testing for GCP Serverless

Cloud Run is Google Cloud's fully managed serverless platform. You deploy a container, and GCP handles scaling, traffic routing, and infrastructure. Testing Cloud Run services requires a slightly different approach than traditional servers — because your service might handle 1 request or 10,000, scale to zero between requests, and call other GCP services it can't reach locally.

This guide covers how to test Cloud Run services at every layer: unit tests, local container testing, integration tests against real GCP services, and load testing for scaling behavior.

Understanding What Cloud Run Changes About Testing

Cloud Run introduces specific testing challenges:

Cold starts: Your container starts from zero when traffic arrives. Initialization code runs on every cold start. Bugs in initialization aren't always caught by unit tests.

Container health: Cloud Run expects HTTP traffic on a specific port. If your container doesn't respond to health checks, it fails deployment — even if the application logic is correct.

Service-to-service auth: Cloud Run services authenticate to each other via IAM. Missing IAM configuration causes runtime failures that are invisible in local testing.

Concurrency: Cloud Run sends multiple requests to a single container instance simultaneously. Race conditions that only appear under concurrent load won't show up in sequential unit tests.

Plan your testing strategy around these characteristics.

Unit Testing Cloud Run Services

Unit tests for Cloud Run are the same as for any containerized service. The key is isolating GCP service dependencies with mocks.

# app.py — simple Cloud Run service
import os
from flask import Flask, request, jsonify
from google.cloud import firestore

app = Flask(__name__)

@app.route('/users/<user_id>', methods=['GET'])
def get_user(user_id):
    db = firestore.Client()
    doc = db.collection('users').document(user_id).get()

    if not doc.exists:
        return jsonify({'error': 'User not found'}), 404

    return jsonify(doc.to_dict()), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))

Unit test with mocked Firestore:

# test_app.py
import pytest
from unittest.mock import patch, MagicMock
from app import app

@pytest.fixture
def client():
    app.config['TESTING'] = True
    with app.test_client() as client:
        yield client

def test_get_user_returns_user_data(client):
    mock_doc = MagicMock()
    mock_doc.exists = True
    mock_doc.to_dict.return_value = {
        'id': 'u001',
        'name': 'Alice',
        'email': 'alice@example.com',
    }

    with patch('app.firestore.Client') as mock_firestore:
        mock_firestore.return_value.collection.return_value \
            .document.return_value.get.return_value = mock_doc

        response = client.get('/users/u001')

        assert response.status_code == 200
        data = response.get_json()
        assert data['name'] == 'Alice'

def test_get_user_returns_404_for_missing_user(client):
    mock_doc = MagicMock()
    mock_doc.exists = False

    with patch('app.firestore.Client') as mock_firestore:
        mock_firestore.return_value.collection.return_value \
            .document.return_value.get.return_value = mock_doc

        response = client.get('/users/nonexistent')

        assert response.status_code == 404
        assert 'error' in response.get_json()

def test_health_check_responds(client):
    """Cloud Run requires health check to pass."""
    response = client.get('/')
    assert response.status_code in [200, 204]

Local Container Testing

Before deploying to Cloud Run, verify your container runs correctly locally:

# Build the container
docker build -t my-service:test .

# Run locally on the port Cloud Run expects
docker run -p 8080:8080 \
  -e PORT=8080 \
  -e GOOGLE_APPLICATION_CREDENTIALS=/credentials.json \
  -v ~/.config/gcloud/application_default_credentials.json:/credentials.json:ro \
  my-service:test

Test the running container:

# Health check
curl -f http://localhost:8080/

# Functional test
curl http://localhost:8080/users/u001

# Test with realistic load
ab -n 100 -c 10 http://localhost:8080/users/u001

Write a container smoke test script:

#!/bin/bash
# test-container.sh

IMAGE="my-service:test"
PORT=8080

# Start container
CONTAINER=$(docker run -d -p $PORT:$PORT $IMAGE)

# Wait for container to be ready
until curl -sf http://localhost:$PORT/ > /dev/null; do
  sleep 0.5
done

echo "Container started. Running smoke tests..."

# Test 1: Health check
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$PORT/)
[ "$STATUS" = "200" ] || { echo "FAIL: health check returned $STATUS"; docker stop $CONTAINER; exit 1; }
echo "PASS: health check"

# Test 2: 404 for missing resource
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$PORT/users/does-not-exist)
[ "$STATUS" = "404" ] || { echo "FAIL: expected 404, got $STATUS"; docker stop $CONTAINER; exit 1; }
echo "PASS: 404 for missing resource"

# Cleanup
docker stop $CONTAINER
echo "All smoke tests passed."

Testing With the Cloud Run Emulator

For local integration testing with GCP services, use the GCP emulators:

# Start Firestore emulator
gcloud emulators firestore start --host-port=localhost:8200

# Start Pub/Sub emulator
gcloud emulators pubsub start --host-port=localhost:8085

Configure your application to use the emulator:

# test_integration.py
import os
import pytest

# Point SDK to emulators
os.environ['FIRESTORE_EMULATOR_HOST'] = 'localhost:8200'
os.environ['PUBSUB_EMULATOR_HOST'] = 'localhost:8085'

from google.cloud import firestore
from app import app

@pytest.fixture(scope='session')
def firestore_client():
    client = firestore.Client(project='test-project')
    yield client
    # Cleanup test data
    # (emulator resets between test runs if you restart it)

def test_create_and_retrieve_user(firestore_client):
    """Full integration test using Firestore emulator."""
    # Seed test data
    firestore_client.collection('users').document('u001').set({
        'name': 'Bob',
        'email': 'bob@example.com',
    })

    # Test the service
    app.config['TESTING'] = True
    with app.test_client() as client:
        response = client.get('/users/u001')

        assert response.status_code == 200
        assert response.get_json()['name'] == 'Bob'

Testing Cloud Run Deployments

Test that your Cloud Run deployment is healthy after each deploy:

# deploy-and-test.sh
SERVICE_NAME="my-service"
REGION="us-central1"
PROJECT="my-project"

# Deploy
gcloud run deploy $SERVICE_NAME \
  --image gcr.io/$PROJECT/$SERVICE_NAME:$GITHUB_SHA \
  --region $REGION \
  --platform managed

# Get the service URL
SERVICE_URL=$(gcloud run services describe $SERVICE_NAME \
  --region $REGION \
  --format='value(status.url)')

echo "Deployed to $SERVICE_URL"

# Wait for deployment health
/usr/local/bin/await "curl -sf $SERVICE_URL/ > /dev/null"

# Smoke tests against live service
python3 -m pytest tests/smoke/ -v \
  --base-url="$SERVICE_URL" \
  --timeout=30

Smoke test that runs against the deployed service:

# tests/smoke/test_deployed_service.py
import pytest
import requests

@pytest.fixture
def base_url(request):
    return request.config.getoption('--base-url')

def test_health_check(base_url):
    response = requests.get(base_url, timeout=10)
    assert response.status_code == 200

def test_service_handles_valid_request(base_url):
    # Use a known test record seeded in the test environment
    response = requests.get(f'{base_url}/users/test-user', timeout=10)
    assert response.status_code in [200, 404]  # Either is valid, 500 is not
    assert response.headers.get('Content-Type') == 'application/json'

Load Testing for Cloud Run Scaling

Cloud Run scales based on concurrent requests. Load testing verifies your service scales correctly and handles traffic spikes:

# Install k6
brew install k6  # macOS

# Create load test script
cat > load-test.js << 'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 10 },   // Ramp up to 10 users
    { duration: '60s', target: 50 },   // Ramp to 50 users (triggers scaling)
    { duration: '30s', target: 0 },    // Scale back down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],   // 95% of requests under 500ms
    http_req_failed: ['rate<0.01'],     // Less than 1% error rate
  },
};

export default function () {
  const response = http.get(`${__ENV.SERVICE_URL}/users/u001`);

  check(response, {
    'status is 200 or 404': (r) => [200, 404].includes(r.status),
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  sleep(1);
}
EOF

# Run load test
k6 run -e SERVICE_URL=$SERVICE_URL load-test.js

Check Cloud Run metrics in GCP Console after the load test:

  • Request count: Did scaling happen as expected?
  • Instance count: How many instances spun up?
  • Latency P95: Did cold starts cause latency spikes?
  • Error rate: Any 500s during scale-out?

CI/CD Integration

# .github/workflows/cloud-run.yml
name: Cloud Run CI/CD

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      firestore-emulator:
        image: gcr.io/google.com/cloudsdktool/cloud-sdk:emulators
        ports:
          - 8200:8200

    steps:
      - uses: actions/checkout@v4

      - name: Unit tests
        run: pytest tests/unit/ -v

      - name: Integration tests (with emulator)
        env:
          FIRESTORE_EMULATOR_HOST: localhost:8200
        run: pytest tests/integration/ -v

      - name: Build and push container
        run: |
          docker build -t gcr.io/${{ secrets.GCP_PROJECT }}/my-service:${{ github.sha }} .
          docker push gcr.io/${{ secrets.GCP_PROJECT }}/my-service:${{ github.sha }}

      - name: Container smoke test
        run: bash test-container.sh

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy my-service \
            --image gcr.io/${{ secrets.GCP_PROJECT }}/my-service:${{ github.sha }} \
            --region us-central1

      - name: Smoke test deployment
        run: pytest tests/smoke/ --base-url=${{ env.SERVICE_URL }}

Key Takeaways

Cloud Run testing works best in layers:

  1. Unit tests mock GCP services — fast, catch logic bugs
  2. Container tests verify the image runs correctly — catch packaging issues
  3. Emulator integration tests test real GCP service interactions locally — catch integration bugs
  4. Deployment smoke tests verify the live service — catch configuration and IAM issues
  5. Load tests verify scaling behavior — catch concurrency and cold start issues

Each layer catches different failure modes. Skip any layer and you'll find its bugs in production.


HelpMeTest can run automated behavioral tests against your Cloud Run services after each deployment, giving you continuous verification of production behavior. Start free →

Start now free