Testing Firebase Functions in Python

This guide explains how to effectively test Firebase Functions in Python using pytest and mocking.

Overview

Firebase Functions decorated with @https_fn.on_request() can be challenging to test because they expect specific request formats. This guide provides utilities and patterns to make testing easier.

Key Testing Strategies

Call the function directly with mock request objects, bypassing the Firebase Functions framework:

from firebase_test_utils import FirebaseFunctionTester
from main import http_generate_pin

def test_http_generate_pin():
    # Create a mock request
    request = FirebaseFunctionTester.create_authorized_request(
        "http_generate_pin",
        query_params={"email": "test@example.com", "uid": "test-uid"}
    )

    # Mock external dependencies
    with patch('main.send_email') as mock_send_email:
        mock_send_email.return_value = True

        # Call function directly
        response = http_generate_pin(request)

        # Assert results
        assert response.status_code == 200
        assert response.data == b"OK"

2. Integration Testing with Flask Test Client

For more comprehensive testing that includes the HTTP layer:

from flask import Flask
from werkzeug.test import Client

def test_with_flask_client():
    app = Flask(__name__)

    # Register your function as a route
    @app.route('/test_function', methods=['POST'])
    def test_route():
        from flask import request
        return http_generate_pin(request)

    client = Client(app)
    response = client.post('/test_function?email=test@example.com&uid=test-uid',
                          headers={'api_key': 'secret'})

    assert response.status_code == 200

3. Mock External Dependencies

Always mock external services to avoid side effects:

def test_with_mocked_dependencies(mock_firestore):
    with patch('main.send_email') as mock_email, \
         patch('main.get_db', return_value=mock_firestore):

        # Your test code here
        response = http_generate_pin(request)

        # Verify mocks were called correctly
        mock_email.assert_called_once()
        mock_firestore.collection.assert_called_with('users')

Testing Utilities

FirebaseFunctionTester

Helper class for creating mock requests:

  • create_authorized_request() - Creates request with valid API key
  • create_unauthorized_request() - Creates request with invalid/missing API key

MockFirestoreHelper

Helper for mocking Firestore operations:

  • create_mock_firestore() - Creates properly configured mock Firestore client
  • assert_document_set() - Asserts that documents were set with expected data

Common Testing Patterns

1. Test Happy Path

def test_successful_operation(mock_firestore):
    AppService.instance().database = mock_firestore

    request = FirebaseFunctionTester.create_authorized_request(
        "your_function",
        query_params={"param1": "value1"}
    )

    with patch('main.external_service') as mock_service:
        response = your_function(request)

        assert response.status_code == 200
        mock_service.assert_called_once()

2. Test Error Cases

def test_invalid_input():
    request = FirebaseFunctionTester.create_authorized_request(
        "your_function",
        query_params={"param1": "invalid_value"}
    )

    response = your_function(request)

    assert response.status_code == 400
    assert b"Invalid" in response.data

3. Test Authorization

def test_unauthorized_access():
    request = FirebaseFunctionTester.create_unauthorized_request(
        "your_function",
        api_key="wrong_key"
    )

    response = your_function(request)

    assert response.status_code == 403

Best Practices

  1. Always mock external dependencies - Don’t send real emails, make real API calls, or write to real databases during tests
  2. Test both success and failure paths - Include tests for invalid inputs, unauthorized access, and error conditions
  3. Use descriptive test names - Make it clear what scenario each test is covering
  4. Keep tests isolated - Each test should be independent and not rely on the state from other tests
  5. Use fixtures for common setup - Create pytest fixtures for commonly used mock objects
  6. Verify interactions - Assert that mocked services were called with expected parameters

Running Tests

# Run all tests
pytest test/

# Run specific test file
pytest test/main_test.py

# Run with verbose output
pytest -v test/

# Run with coverage
pytest --cov=main test/

Common Issues and Solutions

Issue: “Unhandled error: 400 Bad Request”

Problem: Trying to use Flask’s test client directly with Firebase Functions.

Solution: Use the direct function testing approach shown above.

Issue: Mock not being called

Problem: The import path in the patch is incorrect.

Solution: Use the path where the function is used, not where it’s defined:

# If main.py imports and uses send_email
with patch('main.send_email'):  # Correct

# Not this:
with patch('some_module.send_email'):  # Wrong if main.py imports it

Issue: Firebase Functions framework errors

Problem: Tests fail because Firebase Functions framework isn’t properly initialized.

Solution: Mock the framework or test functions directly without the decorator.

Example Test File Structure

test/
├── __init__.py
├── conftest.py              # Shared fixtures
├── firebase_test_utils.py   # Testing utilities
├── main_test.py            # Tests for main.py functions
├── test_auth.py            # Authentication function tests
├── test_payments.py        # Payment function tests
└── README_TESTING.md       # This file