Mocking GCP service clients in unit tests is the key to isolating your Cloud Function’s logic from external dependencies, allowing for faster, more reliable, and cheaper testing.
Let’s see this in action. Imagine a Cloud Function that writes a message to Firestore and then publishes that message to Pub/Sub.
# main.py
from google.cloud import firestore
from google.cloud import pubsub_v1
import json
def process_message(request):
request_json = request.get_json()
message_data = request_json.get('data')
document_id = request_json.get('document_id')
if not message_data or not document_id:
return 'Missing data or document_id', 400
# Write to Firestore
db = firestore.Client()
doc_ref = db.collection('messages').document(document_id)
doc_ref.set({'data': message_data, 'processed': True})
# Publish to Pub/Sub
publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path('your-gcp-project-id', 'your-topic-id')
publisher.publish(topic_path, json.dumps({'id': document_id, 'content': message_data}).encode('utf-8'))
return 'Message processed successfully', 200
Now, how do we unit test this without actually hitting GCP? We’ll use Python’s unittest.mock library.
# test_main.py
import unittest
from unittest.mock import patch, MagicMock
from main import process_message
class TestProcessMessage(unittest.TestCase):
@patch('main.firestore.Client')
@patch('main.pubsub_v1.PublisherClient')
def test_process_message_success(self, MockPublisherClient, MockFirestoreClient):
# Mock Firestore client and its methods
mock_firestore_client_instance = MockFirestoreClient.return_value
mock_doc_ref = MagicMock()
mock_firestore_client_instance.collection.return_value.document.return_value = mock_doc_ref
# Mock Pub/Sub client and its methods
mock_publisher_client_instance = MockPublisherClient.return_value
mock_topic_path = "projects/your-gcp-project-id/topics/your-topic-id"
mock_publisher_client_instance.topic_path.return_value = mock_topic_path
# Simulate an incoming request
mock_request = MagicMock()
mock_request.get_json.return_value = {
'data': 'test data content',
'document_id': 'doc123'
}
# Call the function
status_code = process_message(mock_request)
# Assertions
# Firestore assertions
MockFirestoreClient.assert_called_once()
mock_firestore_client_instance.collection.assert_called_once_with('messages')
mock_firestore_client_instance.collection.return_value.document.assert_called_once_with('doc123')
mock_doc_ref.set.assert_called_once_with({'data': 'test data content', 'processed': True})
# Pub/Sub assertions
MockPublisherClient.assert_called_once()
mock_publisher_client_instance.topic_path.assert_called_once_with('your-gcp-project-id', 'your-topic-id')
mock_publisher_client_instance.publish.assert_called_once_with(
mock_topic_path,
b'{"id": "doc123", "content": "test data content"}'
)
self.assertEqual(status_code, 'Message processed successfully')
if __name__ == '__main__':
unittest.main()
This test works by replacing the actual google.cloud.firestore.Client and google.cloud.pubsub_v1.PublisherClient with unittest.mock.MagicMock objects. We then configure these mocks to behave as expected: MockFirestoreClient.return_value represents the instance of the Firestore client that would be created, and we tell its collection().document() chain what to return. Similarly, we mock the PublisherClient instance and its topic_path and publish methods.
The @patch decorator is crucial here. It tells unittest to replace the specified object with a mock for the duration of the test function. We can then use assert_called_once_with to verify that our function interacted with these mocked clients exactly as intended, passing the correct arguments. This gives us confidence that our function’s logic is sound, without incurring any actual GCP costs or latency.
The real power comes from the fact that your Cloud Function’s code doesn’t need to know it’s being tested. The google.cloud library is designed to be dependency-injectable. When you write db = firestore.Client(), you’re essentially calling a factory that could return a real client or, in our test environment, a mock. The @patch decorator intercepts that factory call.
When you’re dealing with functions that have complex initialization or require specific configurations for their clients (like setting credentials or specifying regions), you can pass those configurations directly into the mock’s return_value or configure the mock’s methods to accept them. For example, if your firestore.Client() needed an explicit project argument, you’d mock it like MockFirestoreClient.return_value = firestore.Client(project='mock-project') and then assert that MockFirestoreClient was called with the correct arguments.
The next hurdle you’ll likely face is testing functions that handle asynchronous operations, where you’ll need to mock async methods and ensure your test runner can handle asyncio event loops.