Пирамида тестирования
/\
/ \ E2E Tests (мало, медленные, дорогие)
/ \ Проверяют всю систему целиком
/──────\
/ \ Integration Tests (средне)
/ \ Проверяют взаимодействие компонентов
/────────────\
/ \ Unit Tests (много, быстрые, дешёвые)
/________________\ Проверяют отдельные функции
Unit Tests для Lambda
Установка зависимостей
pip install pytest moto boto3
Тесты с moto (mock AWS)
# tests/test_visitor_counter.py
import json
import boto3
from decimal import Decimal
from moto import mock_aws
import pytest
# Import the handler
from backend.lambda_function import lambda_handler
@pytest.fixture
def dynamodb_table():
"""Create mock DynamoDB table."""
with mock_aws():
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.create_table(
TableName='visitor-counter',
KeySchema=[{'AttributeName': 'id', 'KeyType': 'HASH'}],
AttributeDefinitions=[{'AttributeName': 'id', 'AttributeType': 'S'}],
BillingMode='PAY_PER_REQUEST'
)
# Seed initial data
table.put_item(Item={'id': 'visitors', 'count': Decimal('0')})
yield table
@mock_aws
def test_counter_increments(dynamodb_table):
"""Test that counter increments by 1."""
event = {'httpMethod': 'GET', 'path': '/count'}
response = lambda_handler(event, None)
assert response['statusCode'] == 200
body = json.loads(response['body'])
assert body['count'] == 1
@mock_aws
def test_counter_increments_multiple(dynamodb_table):
"""Test multiple increments."""
event = {'httpMethod': 'GET', 'path': '/count'}
# Call 3 times
for expected in [1, 2, 3]:
response = lambda_handler(event, None)
body = json.loads(response['body'])
assert body['count'] == expected
@mock_aws
def test_response_has_cors_headers(dynamodb_table):
"""Test CORS headers are present."""
event = {'httpMethod': 'GET', 'path': '/count'}
response = lambda_handler(event, None)
assert 'Access-Control-Allow-Origin' in response['headers']
@mock_aws
def test_response_is_json(dynamodb_table):
"""Test response is valid JSON with count field."""
event = {'httpMethod': 'GET', 'path': '/count'}
response = lambda_handler(event, None)
body = json.loads(response['body'])
assert 'count' in body
assert isinstance(body['count'], int)
assert body['count'] >= 0
Запуск тестов
# Запуск всех тестов
python -m pytest tests/ -v
# С покрытием
python -m pytest tests/ -v --cov=backend --cov-report=term-missing
# Результат:
# tests/test_visitor_counter.py::test_counter_increments PASSED
# tests/test_visitor_counter.py::test_counter_increments_multiple PASSED
# tests/test_visitor_counter.py::test_response_has_cors_headers PASSED
# tests/test_visitor_counter.py::test_response_is_json PASSED
# 4 passed in 1.23s
Integration Tests
Интеграционные тесты проверяют взаимодействие компонентов в реальном облаке:
# tests/test_integration.py
import requests
import pytest
API_URL = 'https://api.ivanpetrov.com/count'
FRONTEND_URL = 'https://resume.ivanpetrov.com'
class TestAPIIntegration:
"""Integration tests against live API."""
def test_api_returns_200(self):
response = requests.get(API_URL)
assert response.status_code == 200
def test_api_returns_count(self):
response = requests.get(API_URL)
data = response.json()
assert 'count' in data
assert isinstance(data['count'], int)
def test_api_cors_headers(self):
headers = {'Origin': FRONTEND_URL}
response = requests.get(API_URL, headers=headers)
assert 'access-control-allow-origin' in response.headers
def test_count_increments(self):
"""Two calls should return incrementing values."""
r1 = requests.get(API_URL).json()['count']
r2 = requests.get(API_URL).json()['count']
assert r2 >= r1 # May not be exactly +1 due to concurrent users
def test_api_response_time(self):
"""API should respond within 2 seconds."""
response = requests.get(API_URL)
assert response.elapsed.total_seconds() < 2.0
class TestFrontendIntegration:
"""Integration tests for frontend."""
def test_frontend_returns_200(self):
response = requests.get(FRONTEND_URL)
assert response.status_code == 200
def test_frontend_has_html(self):
response = requests.get(FRONTEND_URL)
assert 'text/html' in response.headers.get('content-type', '')
def test_frontend_https(self):
response = requests.get(FRONTEND_URL)
assert response.url.startswith('https://')
def test_frontend_has_counter_element(self):
response = requests.get(FRONTEND_URL)
assert 'visitor-count' in response.text
E2E Tests
E2E тесты проверяют систему глазами пользователя:
# tests/test_e2e.py (с Playwright)
from playwright.sync_api import sync_playwright
import pytest
FRONTEND_URL = 'https://resume.ivanpetrov.com'
def test_visitor_counter_displays():
"""End-to-end test: page loads and counter appears."""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(FRONTEND_URL)
# Wait for counter to load
counter = page.locator('#visitor-count')
counter.wait_for(state='visible', timeout=5000)
# Counter should be a number
text = counter.text_content()
assert text.isdigit(), f"Counter text is not a number: {text}"
assert int(text) > 0
browser.close()
def test_page_title():
"""Page should have a meaningful title."""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(FRONTEND_URL)
assert page.title() != ''
assert len(page.title()) > 5
browser.close()
Тестирование в CI/CD
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: |
pip install -r requirements-test.txt
python -m pytest tests/unit/ -v --cov=backend
integration-tests:
needs: unit-tests
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: |
pip install requests pytest
python -m pytest tests/integration/ -v
Что тестировать в CRC
| Тип теста | Что проверяем | Инструменты |
|---|---|---|
| Unit | Lambda handler, input/output, edge cases | pytest, moto |
| Integration | API endpoint, CORS, response format | requests, pytest |
| E2E | Страница грузится, счётчик работает | Playwright, Selenium |
| Performance | Время ответа < 2 секунд | requests, locust |
| Security | Нет XSS, CORS правильный, HTTPS | curl, OWASP ZAP |