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:testTest 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/u001Write 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:8085Configure 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=30Smoke 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.jsCheck 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:
- Unit tests mock GCP services — fast, catch logic bugs
- Container tests verify the image runs correctly — catch packaging issues
- Emulator integration tests test real GCP service interactions locally — catch integration bugs
- Deployment smoke tests verify the live service — catch configuration and IAM issues
- 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 →