MidПрактика4 min

Тестирование в облаке

Unit-тесты, интеграционные тесты и E2E-тесты для serverless-функций CRC

Пирамида тестирования

        /\
       /  \      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

Проверь себя

Какой инструмент используется для E2E-тестирования CRC в браузере?

Почему unit-тесты находятся в основании пирамиды тестирования?

Когда в CI/CD должны запускаться integration-тесты?

Почему в integration-тесте 'test_count_increments' используется assert r2 >= r1, а не r2 == r1 + 1?

Что такое moto в контексте тестирования AWS Lambda?