Тестирование бессерверных функций
Updated Jul 2026
Вызовы тестирования бессерверных архитектур
Бессерверные функции выполняются в окружениях, которые вы не контролируете. Вы не можете подключиться к машине по SSH, не можете проверить файловую систему и не можете присоединить отладчик в продакшене. Среда выполнения эфемерна — она существует только на время вызова.
Это означает, что ваша стратегия тестирования должна быть более строгой, чем при тестировании традиционных приложений. Если вы пропустите ошибку в бессерверной функции, вашими единственными инструментами отладки будут логи и трейсы. Нет никакого «давайте быстро проверим на сервере».
AWS Lambda с SAM Local
AWS SAM (Serverless Application Model) обеспечивает локальную эмуляцию функций Lambda. SAM Local запускает Docker-контейнер, имитирующий среду выполнения Lambda, что позволяет тестировать функции локально перед развёртыванием в AWS.
Локальный вызов
# Invoke a single function locally with a test event
sam local invoke MyFunction --event events/api-gateway.json
# Start a local API Gateway emulator
sam local start-api --port 3000
# Run tests against the local emulator
pytest tests/integration/ --base-url http://localhost:3000
# Generate a sample event for testing
sam local generate-event apigateway aws-proxy > events/api-gateway.json
sam local generate-event sqs receive-message > events/sqs-message.json
sam local generate-event s3 put > events/s3-put.json
Модульное тестирование обработчиков Lambda
Обработчики Lambda — это обычные функции. Тестируйте их как любые другие функции, мокая AWS-сервисы, которые они вызывают:
# tests/unit/test_handler.py
import json
import pytest
from unittest.mock import patch, MagicMock
from src.handlers.order_processor import lambda_handler
def test_order_processing_success():
"""Test successful order processing."""
event = {
"Records": [{
"body": json.dumps({
"orderId": "ORD-123",
"items": [{"sku": "WIDGET-A", "qty": 2}],
"customerId": "CUST-456"
})
}]
}
context = MagicMock()
context.function_name = "order-processor"
context.memory_limit_in_mb = 256
context.get_remaining_time_in_millis.return_value = 30000
result = lambda_handler(event, context)
assert result["statusCode"] == 200
body = json.loads(result["body"])
assert body["orderId"] == "ORD-123"
assert body["status"] == "processed"
def test_order_processing_invalid_payload():
"""Test handling of malformed input."""
event = {"Records": [{"body": "not json"}]}
context = MagicMock()
result = lambda_handler(event, context)
assert result["statusCode"] == 400
body = json.loads(result["body"])
assert "error" in body
def test_order_processing_missing_required_fields():
"""Test handling of missing required fields."""
event = {
"Records": [{
"body": json.dumps({
"orderId": "ORD-123"
# Missing: items and customerId
})
}]
}
context = MagicMock()
result = lambda_handler(event, context)
assert result["statusCode"] == 400
@patch("src.handlers.order_processor.dynamodb_client")
def test_order_persisted_to_dynamodb(mock_dynamo):
"""Test that processed orders are saved to DynamoDB."""
event = {
"Records": [{
"body": json.dumps({
"orderId": "ORD-123",
"items": [{"sku": "WIDGET-A", "qty": 2}],
"customerId": "CUST-456"
})
}]
}
context = MagicMock()
lambda_handler(event, context)
mock_dynamo.put_item.assert_called_once()
call_args = mock_dynamo.put_item.call_args
item = call_args["Item"]
assert item["orderId"]["S"] == "ORD-123"
assert item["status"]["S"] == "processed"
Тестирование Google Cloud Functions
Google Cloud Functions можно тестировать локально с использованием Functions Framework:
# Install the Functions Framework for local testing
pip install functions-framework
# Run locally
functions-framework --target=process_order --port=8080
# Test with curl
curl -X POST http://localhost:8080 \
-H "Content-Type: application/json" \
-d '{"orderId": "ORD-123"}'
Тестирование Cloud Functions с pytest
# tests/test_cloud_function.py
import pytest
from unittest.mock import patch, MagicMock
from flask import Flask
import json
# Import the function
from main import process_order
@pytest.fixture
def app():
return Flask(__name__)
@pytest.fixture
def client(app):
return app.test_client()
def test_process_order_valid_request(app):
"""Test processing a valid order."""
with app.test_request_context(
method="POST",
json={"orderId": "ORD-123", "amount": 99.99}
):
from flask import request
response = process_order(request)
data = json.loads(response)
assert data["status"] == "processed"
def test_process_order_missing_order_id(app):
"""Test handling of missing order ID."""
with app.test_request_context(
method="POST",
json={"amount": 99.99}
):
from flask import request
response, status_code = process_order(request)
assert status_code == 400
Вызовы и решения при тестировании бессерверных архитектур
| Вызов | Решение | Инструмент/Подход |
|---|---|---|
| Задержка холодного старта | Измерять p99-задержку в нагрузочных тестах | Artillery, k6 с целевыми Lambda |
| Поведение при таймауте | Устанавливать таймаут функции ниже таймаута теста | Свойство Timeout в SAM-шаблоне |
| Ограничения памяти | Профилировать использование памяти при ожидаемых размерах payload | Lambda Power Tuning |
| Лимиты параллелизма | Нагрузочное тестирование с зарезервированным параллелизмом | k6 со сценариями нарастания нагрузки |
| Разрешения IAM | Тестировать с ролью минимальных привилегий локально | SAM local с профилями учётных данных |
| Маппинг источников событий | Тестировать с реалистичными event payload | Воспроизведение событий из CloudWatch |
| Идемпотентность | Отправлять дублирующие события, проверять отсутствие двойной обработки | Интеграционные тесты с логикой повторов |
Тестирование холодного старта
# tests/performance/test_cold_start.py
import time
import boto3
import statistics
def measure_cold_start(function_name, num_invocations=10):
"""Measure cold start latency by forcing new execution environments."""
client = boto3.client("lambda")
latencies = []
for i in range(num_invocations):
# Update environment variable to force a new container
client.update_function_configuration(
FunctionName=function_name,
Environment={
"Variables": {
"COLD_START_TRIGGER": str(time.time())
}
}
)
# Wait for update to propagate
time.sleep(5)
start = time.time()
response = client.invoke(
FunctionName=function_name,
Payload=b'{"test": true}',
)
elapsed_ms = (time.time() - start) * 1000
latencies.append(elapsed_ms)
return {
"p50": statistics.median(latencies),
"p95": sorted(latencies)[int(len(latencies) * 0.95)],
"p99": sorted(latencies)[int(len(latencies) * 0.99)],
"max": max(latencies),
}
Тестирование таймаутов
def test_function_completes_before_timeout():
"""Verify the function completes within its configured timeout."""
import time
FUNCTION_TIMEOUT = 30 # seconds (from SAM template)
SAFETY_MARGIN = 5 # seconds
start = time.time()
result = lambda_handler(large_event, context)
elapsed = time.time() - start
assert elapsed < (FUNCTION_TIMEOUT - SAFETY_MARGIN), \
f"Function took {elapsed:.1f}s, dangerously close to {FUNCTION_TIMEOUT}s timeout"
Тестирование с LocalStack
LocalStack предоставляет локальный стек AWS для тестирования бессерверных функций с реальной эмуляцией AWS-сервисов:
# tests/integration/test_lambda_localstack.py
import boto3
import json
import pytest
@pytest.fixture(scope="module")
def aws_clients():
"""Create AWS clients pointing to LocalStack."""
endpoint = "http://localhost:4566"
return {
"lambda": boto3.client("lambda", endpoint_url=endpoint,
region_name="us-east-1",
aws_access_key_id="test",
aws_secret_access_key="test"),
"sqs": boto3.client("sqs", endpoint_url=endpoint,
region_name="us-east-1",
aws_access_key_id="test",
aws_secret_access_key="test"),
"dynamodb": boto3.client("dynamodb", endpoint_url=endpoint,
region_name="us-east-1",
aws_access_key_id="test",
aws_secret_access_key="test"),
}
def test_lambda_processes_sqs_message(aws_clients):
"""Test the full flow: SQS message triggers Lambda, result in DynamoDB."""
sqs = aws_clients["sqs"]
dynamodb = aws_clients["dynamodb"]
# Send message to SQS
queue_url = sqs.create_queue(QueueName="orders")["QueueUrl"]
sqs.send_message(
QueueUrl=queue_url,
MessageBody=json.dumps({"orderId": "ORD-TEST", "amount": 42.00})
)
# Wait for Lambda to process (triggered by SQS event source mapping)
import time
time.sleep(5)
# Verify the result in DynamoDB
result = dynamodb.get_item(
TableName="orders",
Key={"orderId": {"S": "ORD-TEST"}}
)
assert "Item" in result
assert result["Item"]["status"]["S"] == "processed"
Комбинация модульных тестов (быстрые, изолированные), локальной эмуляции (SAM Local, Functions Framework) и интеграционных тестов (LocalStack, Testcontainers) обеспечивает всестороннее покрытие бессерверных функций без необходимости развёрнутого облачного окружения для каждого запуска тестов.