Skip to content
IRC-CodingIRC-Coding
основы тестированияUnit TestsIntegration TestsE2E TestsTDDBDD

Основы тестирования: Unit Tests, Integration Tests, E2E, TDD и BDD

Основы Software Testing: Unit Tests, Integration Tests, E2E Tests, TDD и BDD. Практические примеры Test-Driven Development и Behavior-Driven Development.

S

schutzgeist

26 min read
Основы тестирования: Unit Tests, Integration Tests, E2E, TDD и BDD

Основы тестирования ПО: Unit тесты, интеграционные тесты, E2E тесты, TDD и BDD

Этот материал представляет собой подробное введение в основы тестирования ПО, охватывающее Unit тесты, интеграционные тесты, E2E тесты, TDD и BDD с практическими примерами.

Вкратце

Тестирование ПО — это систематический процесс проверки качества и функциональности программного обеспечения, который гарантирует соответствие требованиям и корректность работы.

Определение

Тестирование ПО — это проверка корректности, полноты и качества программного обеспечения через целевые тесты и методы валидации.

Ключевые компоненты:

Unit тесты

  • Изолированные тесты: проверяют отдельные компоненты независимо друг от друга
  • Быстрое выполнение: от миллисекунд до нескольких секунд
  • Mocking: подмена зависимостей
  • Coverage: измерение покрытия кода
  • Frameworks: JUnit, Jest, PyTest

Интеграционные тесты

  • Интеграция компонентов: проверка работы нескольких модулей вместе
  • Тесты БД: валидация слоя персистентности
  • API тесты: проверка интерфейсов между сервисами
  • Интеграция систем: проверка всей системы
  • Окружения: Staging и тестовые системы

End-to-End (E2E) тесты

  • Пользовательские сценарии: проверка полных рабочих потоков
  • Автоматизация браузера: имитация взаимодействия с UI
  • Кроссбраузерность: тестирование в разных браузерах
  • Мобильные тесты: приложения iOS/Android
  • Инструменты: Selenium, Cypress, Playwright

Test-Driven Development (TDD)

  • Red-Green-Refactor: циклический процесс разработки
  • Тесты в первую очередь: написание тестов до реализации
  • Малые шаги: пошаговая разработка
  • Регрессионные тесты: автоматизированный набор тестов
  • Улучшение архитектуры: более качественное проектирование

Behavior-Driven Development (BDD)

  • Синтаксис Gherkin: читаемое описание тестов
  • User Stories: формулирование бизнес-требований
  • Общее понимание: взаимодействие со stakeholders
  • Cucumber: реализация BDD фреймворка
  • Живая документация: актуальная документация

Ключевые моменты

  • Unit тесты: изолированная проверка функций и классов
  • Интеграционные тесты: проверка взаимодействия компонентов
  • E2E тесты: полная проверка пользовательских сценариев от начала до конца
  • TDD: разработка через тесты с циклом Red-Green-Refactor
  • BDD: разработка через поведение с синтаксисом Gherkin
  • Test Pyramid: больше Unit тестов, меньше интеграционных и E2E
  • Mocking: подмена зависимостей для тестирования
  • Code Coverage: измерение охвата кода тестами
  • Качество ПО: стратегии тестирования и обеспечение качества

Основные компоненты

  1. Стратегия тестирования: планирование и приоритизация тестов
  2. Автоматизация тестов: автоматический запуск тестов
  3. Фреймворки тестирования: инструменты и библиотеки
  4. Continuous Testing: интеграция в CI/CD pipelines
  5. Управление тестовыми данными: генерация и управление данными
  6. Отчётность: анализ и документирование результатов
  7. Нагрузочное тестирование: нагрузочные и стресс-тесты
  8. Тестирование безопасности: поиск уязвимостей

Практические примеры

1. Unit тесты с JavaScript и Jest

// mathUtils.js - Zu testende Funktionen
export class MathUtils {
  static add(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new Error('Both arguments must be numbers');
    }
    return a + b;
  }

  static subtract(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new Error('Both arguments must be numbers');
    }
    return a - b;
  }

  static multiply(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new Error('Both arguments must be numbers');
    }
    return a * b;
  }

  static divide(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new Error('Both arguments must be numbers');
    }
    if (b === 0) {
      throw new Error('Division by zero is not allowed');
    }
    return a / b;
  }

  static factorial(n) {
    if (typeof n !== 'number' || n < 0 || !Number.isInteger(n)) {
      throw new Error('Argument must be a non-negative integer');
    }
    if (n === 0 || n === 1) {
      return 1;
    }
    return n * this.factorial(n - 1);
  }

  static fibonacci(n) {
    if (typeof n !== 'number' || n < 0 || !Number.isInteger(n)) {
      throw new Error('Argument must be a non-negative integer');
    }
    if (n === 0) {
      return 0;
    }
    if (n === 1) {
      return 1;
    }
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }

  static isPrime(n) {
    if (typeof n !== 'number' || n < 2 || !Number.isInteger(n)) {
      return false;
    }
    if (n === 2) {
      return true;
    }
    if (n % 2 === 0) {
      return false;
    }
    for (let i = 3; i <= Math.sqrt(n); i += 2) {
      if (n % i === 0) {
        return false;
      }
    }
    return true;
  }

  static gcd(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new Error('Both arguments must be numbers');
    }
    a = Math.abs(a);
    b = Math.abs(b);
    while (b !== 0) {
      const temp = b;
      b = a % b;
      a = temp;
    }
    return a;
  }

  static lcm(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new Error('Both arguments must be numbers');
    }
    if (a === 0 || b === 0) {
      return 0;
    }
    return Math.abs(a * b) / this.gcd(a, b);
  }
}

// userService.js - Service mit Abhängigkeiten
export class UserService {
  constructor(apiClient, cache) {
    this.apiClient = apiClient;
    this.cache = cache;
  }

  async getUser(id) {
    if (!id) {
      throw new Error('User ID is required');
    }

    // Check cache first
    const cachedUser = this.cache.get(`user:${id}`);
    if (cachedUser) {
      return cachedUser;
    }

    // Fetch from API
    const user = await this.apiClient.get(`/users/${id}`);
    
    // Cache the result
    this.cache.set(`user:${id}`, user, 300); // 5 minutes
    
    return user;
  }

  async createUser(userData) {
    if (!userData || !userData.email || !userData.name) {
      throw new Error('Email and name are required');
    }

    const user = await this.apiClient.post('/users', userData);
    
    // Cache the new user
    this.cache.set(`user:${user.id}`, user, 300);
    
    return user;
  }

  async updateUser(id, userData) {
    if (!id || !userData) {
      throw new Error('User ID and data are required');
    }

    const user = await this.apiClient.put(`/users/${id}`, userData);
    
    // Update cache
    this.cache.set(`user:${id}`, user, 300);
    
    return user;
  }

  async deleteUser(id) {
    if (!id) {
      throw new Error('User ID is required');
    }

    await this.apiClient.delete(`/users/${id}`);
    
    // Remove from cache
    this.cache.delete(`user:${id}`);
    
    return true;
  }

  async searchUsers(query) {
    if (!query) {
      throw new Error('Search query is required');
    }

    const cacheKey = `search:${query}`;
    const cachedResults = this.cache.get(cacheKey);
    if (cachedResults) {
      return cachedResults;
    }

    const results = await this.apiClient.get('/users/search', { q: query });
    
    this.cache.set(cacheKey, results, 600); // 10 minutes
    
    return results;
  }
}

// mathUtils.test.js - Unit Tests
import { MathUtils } from './mathUtils';

describe('MathUtils', () => {
  describe('add', () => {
    test('should add two positive numbers', () => {
      expect(MathUtils.add(2, 3)).toBe(5);
      expect(MathUtils.add(10, 20)).toBe(30);
    });

    test('should add negative numbers', () => {
      expect(MathUtils.add(-2, -3)).toBe(-5);
      expect(MathUtils.add(-10, 5)).toBe(-5);
    });

    test('should add zero', () => {
      expect(MathUtils.add(0, 5)).toBe(5);
      expect(MathUtils.add(5, 0)).toBe(5);
      expect(MathUtils.add(0, 0)).toBe(0);
    });

    test('should handle decimal numbers', () => {
      expect(MathUtils.add(1.5, 2.5)).toBe(4.0);
      expect(MathUtils.add(-1.5, 2.5)).toBe(1.0);
    });

    test('should throw error for non-number arguments', () => {
      expect(() => MathUtils.add('2', 3)).toThrow('Both arguments must be numbers');
      expect(() => MathUtils.add(2, '3')).toThrow('Both arguments must be numbers');
      expect(() => MathUtils.add(null, 3)).toThrow('Both arguments must be numbers');
      expect(() => MathUtils.add(undefined, 3)).toThrow('Both arguments must be numbers');
    });
  });

  describe('subtract', () => {
    test('should subtract two positive numbers', () => {
      expect(MathUtils.subtract(5, 3)).toBe(2);
      expect(MathUtils.subtract(10, 20)).toBe(-10);
    });

    test('should subtract negative numbers', () => {
      expect(MathUtils.subtract(-5, -3)).toBe(-2);
      expect(MathUtils.subtract(-10, 5)).toBe(-15);
    });

    test('should throw error for non-number arguments', () => {
      expect(() => MathUtils.subtract('5', 3)).toThrow('Both arguments must be numbers');
    });
  });

  describe('divide', () => {
    test('should divide two positive numbers', () => {
      expect(MathUtils.divide(10, 2)).toBe(5);
      expect(MathUtils.divide(7, 2)).toBe(3.5);
    });

    test('should handle division by zero', () => {
      expect(() => MathUtils.divide(10, 0)).toThrow('Division by zero is not allowed');
    });

    test('should handle negative numbers', () => {
      expect(MathUtils.divide(-10, 2)).toBe(-5);
      expect(MathUtils.divide(10, -2)).toBe(-5);
      expect(MathUtils.divide(-10, -2)).toBe(5);
    });
  });

  describe('factorial', () => {
    test('should calculate factorial of positive numbers', () => {
      expect(MathUtils.factorial(0)).toBe(1);
      expect(MathUtils.factorial(1)).toBe(1);
      expect(MathUtils.factorial(5)).toBe(120);
      expect(MathUtils.factorial(10)).toBe(3628800);
    });

    test('should throw error for negative numbers', () => {
      expect(() => MathUtils.factorial(-1)).toThrow('Argument must be a non-negative integer');
    });

    test('should throw error for non-integers', () => {
      expect(() => MathUtils.factorial(2.5)).toThrow('Argument must be a non-negative integer');
    });
  });

  describe('fibonacci', () => {
    test('should calculate fibonacci numbers', () => {
      expect(MathUtils.fibonacci(0)).toBe(0);
      expect(MathUtils.fibonacci(1)).toBe(1);
      expect(MathUtils.fibonacci(5)).toBe(5);
      expect(MathUtils.fibonacci(10)).toBe(55);
    });

    test('should throw error for negative numbers', () => {
      expect(() => MathUtils.fibonacci(-1)).toThrow('Argument must be a non-negative integer');
    });
  });

  describe('isPrime', () => {
    test('should identify prime numbers correctly', () => {
      expect(MathUtils.isPrime(2)).toBe(true);
      expect(MathUtils.isPrime(3)).toBe(true);
      expect(MathUtils.isPrime(5)).toBe(true);
      expect(MathUtils.isPrime(7)).toBe(true);
      expect(MathUtils.isPrime(11)).toBe(true);
    });

    test('should identify non-prime numbers correctly', () => {
      expect(MathUtils.isPrime(1)).toBe(false);
      expect(MathUtils.isPrime(4)).toBe(false);
      expect(MathUtils.isPrime(6)).toBe(false);
      expect(MathUtils.isPrime(8)).toBe(false);
      expect(MathUtils.isPrime(9)).toBe(false);
      expect(MathUtils.isPrime(10)).toBe(false);
    });
  });

  describe('gcd', () => {
    test('should calculate greatest common divisor', () => {
      expect(MathUtils.gcd(48, 18)).toBe(6);
      expect(MathUtils.gcd(54, 24)).toBe(6);
      expect(MathUtils.gcd(17, 23)).toBe(1);
      expect(MathUtils.gcd(0, 5)).toBe(5);
      expect(MathUtils.gcd(5, 0)).toBe(5);
    });

    test('should handle negative numbers', () => {
      expect(MathUtils.gcd(-48, 18)).toBe(6);
      expect(MathUtils.gcd(48, -18)).toBe(6);
      expect(MathUtils.gcd(-48, -18)).toBe(6);
    });
  });

  describe('lcm', () => {
    test('should calculate least common multiple', () => {
      expect(MathUtils.lcm(4, 6)).toBe(12);
      expect(MathUtils.lcm(21, 6)).toBe(42);
      expect(MathUtils.lcm(3, 5)).toBe(15);
    });

    test('should handle zero', () => {
      expect(MathUtils.lcm(0, 5)).toBe(0);
      expect(MathUtils.lcm(5, 0)).toBe(0);
      expect(MathUtils.lcm(0, 0)).toBe(0);
    });
  });
});

// userService.test.js - Unit Tests mit Mocking
import { UserService } from './userService';

// Mock dependencies
const mockApiClient = {
  get: jest.fn(),
  post: jest.fn(),
  put: jest.fn(),
  delete: jest.fn()
};

const mockCache = {
  get: jest.fn(),
  set: jest.fn(),
  delete: jest.fn()
};

describe('UserService', () => {
  let userService;

  beforeEach(() => {
    // Reset mocks before each test
    jest.clearAllMocks();
    
    userService = new UserService(mockApiClient, mockCache);
  });

  describe('getUser', () => {
    test('should return user from cache if available', async () => {
      const cachedUser = { id: 1, name: 'John Doe', email: 'john@example.com' };
      mockCache.get.mockReturnValue(cachedUser);

      const result = await userService.getUser(1);

      expect(result).toBe(cachedUser);
      expect(mockCache.get).toHaveBeenCalledWith('user:1');
      expect(mockApiClient.get).not.toHaveBeenCalled();
    });

    test('should fetch user from API if not in cache', async () => {
      const user = { id: 1, name: 'John Doe', email: 'john@example.com' };
      mockCache.get.mockReturnValue(null);
      mockApiClient.get.mockResolvedValue(user);

      const result = await userService.getUser(1);

      expect(result).toBe(user);
      expect(mockCache.get).toHaveBeenCalledWith('user:1');
      expect(mockApiClient.get).toHaveBeenCalledWith('/users/1');
      expect(mockCache.set).toHaveBeenCalledWith('user:1', user, 300);
    });

    test('should throw error if no user ID provided', async () => {
      await expect(userService.getUser()).rejects.toThrow('User ID is required');
    });

    test('should handle API errors', async () => {
      mockCache.get.mockReturnValue(null);
      mockApiClient.get.mockRejectedValue(new Error('API Error'));

      await expect(userService.getUser(1)).rejects.toThrow('API Error');
    });
  });

  describe('createUser', () => {
    test('should create user successfully', async () => {
      const userData = { name: 'John Doe', email: 'john@example.com' };
      const createdUser = { id: 1, ...userData };
      mockApiClient.post.mockResolvedValue(createdUser);

      const result = await userService.createUser(userData);

      expect(result).toBe(createdUser);
      expect(mockApiClient.post).toHaveBeenCalledWith('/users', userData);
      expect(mockCache.set).toHaveBeenCalledWith('user:1', createdUser, 300);
    });

    test('should throw error if email is missing', async () => {
      const userData = { name: 'John Doe' };

      await expect(userService.createUser(userData)).rejects.toThrow('Email and name are required');
    });

    test('should throw error if name is missing', async () => {
      const userData = { email: 'john@example.com' };

      await expect(userService.createUser(userData)).rejects.toThrow('Email and name are required');
    });
  });

  describe('updateUser', () => {
    test('should update user successfully', async () => {
      const userData = { name: 'Jane Doe' };
      const updatedUser = { id: 1, name: 'Jane Doe', email: 'john@example.com' };
      mockApiClient.put.mockResolvedValue(updatedUser);

      const result = await userService.updateUser(1, userData);

      expect(result).toBe(updatedUser);
      expect(mockApiClient.put).toHaveBeenCalledWith('/users/1', userData);
      expect(mockCache.set).toHaveBeenCalledWith('user:1', updatedUser, 300);
    });

    test('should throw error if no user ID provided', async () => {
      await expect(userService.updateUser()).rejects.toThrow('User ID and data are required');
    });
  });

  describe('searchUsers', () => {
    test('should return cached search results if available', async () => {
      const cachedResults = [{ id: 1, name: 'John Doe' }];
      mockCache.get.mockReturnValue(cachedResults);

      const result = await userService.searchUsers('John');

      expect(result).toBe(cachedResults);
      expect(mockCache.get).toHaveBeenCalledWith('search:John');
      expect(mockApiClient.get).not.toHaveBeenCalled();
    });

    test('should search users via API if not in cache', async () => {
      const results = [{ id: 1, name: 'John Doe' }];
      mockCache.get.mockReturnValue(null);
      mockApiClient.get.mockResolvedValue(results);

      const result = await userService.searchUsers('John');

      expect(result).toBe(results);
      expect(mockCache.get).toHaveBeenCalledWith('search:John');
      expect(mockApiClient.get).toHaveBeenCalledWith('/users/search', { q: 'John' });
      expect(mockCache.set).toHaveBeenCalledWith('search:John', results, 600);
    });
  });
});

// Test Configuration
// jest.config.js
module.exports = {
  testEnvironment: 'node',
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/**/*.test.js',
    '!src/**/index.js'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
  testMatch: [
    '**/__tests__/**/*.js',
    '**/?(*.)+(spec|test).js'
  ]
};

// tests/setup.js
// Global test setup
beforeAll(() => {
  console.log('Starting test suite');
});

afterAll(() => {
  console.log('Test suite completed');
});

beforeEach(() => {
  // Reset console mocks
  jest.spyOn(console, 'log').mockImplementation(() => {});
  jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
  // Restore console mocks
  console.log.mockRestore();
  console.error.mockRestore();
});

2. Интеграционные тесты с Python и PyTest

# tests/integration/test_user_integration.py
import pytest
import asyncio
from httpx import AsyncClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from fastapi.testclient import TestClient
from app.main import app
from app.database import get_db, Base
from app.models import User, Post, Comment
from app.core.config import settings

# Test database setup
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Override database dependency for testing
def override_get_db():
    try:
        db = TestingSessionLocal()
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db

@pytest.fixture(scope="session")
def event_loop():
    """Create an instance of the default event loop for the test session."""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()

@pytest.fixture(scope="function")
def db_session():
    """Create a fresh database session for each test."""
    Base.metadata.create_all(bind=engine)
    session = TestingSessionLocal()
    try:
        yield session
    finally:
        session.close()
        Base.metadata.drop_all(bind=engine)

@pytest.fixture(scope="function")
def client(db_session):
    """Create a test client."""
    with TestClient(app) as test_client:
        yield test_client

@pytest.fixture(scope="function")
async def async_client(db_session):
    """Create an async test client."""
    async with AsyncClient(app=app, base_url="http://test") as ac:
        yield ac

@pytest.fixture
def sample_user(db_session):
    """Create a sample user for testing."""
    user = User(
        username="testuser",
        email="test@example.com",
        hashed_password="hashed_password",
        is_active=True
    )
    db_session.add(user)
    db_session.commit()
    db_session.refresh(user)
    return user

@pytest.fixture
def sample_post(db_session, sample_user):
    """Create a sample post for testing."""
    post = Post(
        title="Test Post",
        content="This is a test post content.",
        author_id=sample_user.id,
        is_published=True
    )
    db_session.add(post)
    db_session.commit()
    db_session.refresh(post)
    return post

@pytest.fixture
def sample_comment(db_session, sample_user, sample_post):
    """Create a sample comment for testing."""
    comment = Comment(
        content="This is a test comment.",
        author_id=sample_user.id,
        post_id=sample_post.id
    )
    db_session.add(comment)
    db_session.commit()
    db_session.refresh(comment)
    return comment

class TestUserIntegration:
    """Integration tests for user endpoints."""

    def test_create_user_success(self, client):
        """Test successful user creation."""
        user_data = {
            "username": "newuser",
            "email": "newuser@example.com",
            "password": "securepassword123"
        }

        response = client.post("/api/v1/users/", json=user_data)

        assert response.status_code == 201
        data = response.json()
        assert data["username"] == user_data["username"]
        assert data["email"] == user_data["email"]
        assert "id" in data
        assert "hashed_password" not in data  # Password should not be returned

    def test_create_user_duplicate_email(self, client, sample_user):
        """Test user creation with duplicate email."""
        user_data = {
            "username": "anotheruser",
            "email": sample_user.email,  # Duplicate email
            "password": "securepassword123"
        }

        response = client.post("/api/v1/users/", json=user_data)

        assert response.status_code == 400
        assert "already registered" in response.json()["detail"].lower()

    def test_get_user_success(self, client, sample_user):
        """Test successful user retrieval."""
        response = client.get(f"/api/v1/users/{sample_user.id}")

        assert response.status_code == 200
        data = response.json()
        assert data["id"] == sample_user.id
        assert data["username"] == sample_user.username
        assert data["email"] == sample_user.email

    def test_get_user_not_found(self, client):
        """Test user retrieval with non-existent ID."""
        response = client.get("/api/v1/users/99999")

        assert response.status_code == 404
        assert "not found" in response.json()["detail"].lower()

    def test_update_user_success(self, client, sample_user):
        """Test successful user update."""
        update_data = {
            "username": "updateduser",
            "email": "updated@example.com"
        }

        response = client.put(f"/api/v1/users/{sample_user.id}", json=update_data)

        assert response.status_code == 200
        data = response.json()
        assert data["username"] == update_data["username"]
        assert data["email"] == update_data["email"]

    def test_delete_user_success(self, client, sample_user):
        """Test successful user deletion."""
        response = client.delete(f"/api/v1/users/{sample_user.id}")

        assert response.status_code == 204

        # Verify user is deleted
        response = client.get(f"/api/v1/users/{sample_user.id}")
        assert response.status_code == 404

class TestPostIntegration:
    """Integration tests for post endpoints."""

    def test_create_post_success(self, client, sample_user):
        """Test successful post creation."""
        post_data = {
            "title": "New Test Post",
            "content": "This is a new test post content.",
            "is_published": True
        }

        # Authenticate as sample user
        client.headers.update({"Authorization": f"Bearer {sample_user.id}"})

        response = client.post("/api/v1/posts/", json=post_data)

        assert response.status_code == 201
        data = response.json()
        assert data["title"] == post_data["title"]
        assert data["content"] == post_data["content"]
        assert data["author_id"] == sample_user.id

    def test_get_post_success(self, client, sample_post):
        """Test successful post retrieval."""
        response = client.get(f"/api/v1/posts/{sample_post.id}")

        assert response.status_code == 200
        data = response.json()
        assert data["id"] == sample_post.id
        assert data["title"] == sample_post.title
        assert data["author"]["id"] == sample_post.author_id

    def test_get_posts_with_pagination(self, client, db_session):
        """Test post listing with pagination."""
        # Create multiple posts
        user = db_session.query(User).first()
        for i in range(25):
            post = Post(
                title=f"Post {i}",
                content=f"Content for post {i}",
                author_id=user.id,
                is_published=True
            )
            db_session.add(post)
        db_session.commit()

        response = client.get("/api/v1/posts/?skip=0&limit=10")

        assert response.status_code == 200
        data = response.json()
        assert len(data) == 10
        assert "total" in response.headers

    def test_update_post_success(self, client, sample_post, sample_user):
        """Test successful post update."""
        update_data = {
            "title": "Updated Post Title",
            "content": "Updated post content."
        }

        # Authenticate as post author
        client.headers.update({"Authorization": f"Bearer {sample_user.id}"})

        response = client.put(f"/api/v1/posts/{sample_post.id}", json=update_data)

        assert response.status_code == 200
        data = response.json()
        assert data["title"] == update_data["title"]
        assert data["content"] == update_data["content"]

    def test_delete_post_success(self, client, sample_post, sample_user):
        """Test successful post deletion."""
        # Authenticate as post author
        client.headers.update({"Authorization": f"Bearer {sample_user.id}"})

        response = client.delete(f"/api/v1/posts/{sample_post.id}")

        assert response.status_code == 204

        # Verify post is deleted
        response = client.get(f"/api/v1/posts/{sample_post.id}")
        assert response.status_code == 404

class TestCommentIntegration:
    """Integration tests for comment endpoints."""

    def test_create_comment_success(self, client, sample_post, sample_user):
        """Test successful comment creation."""
        comment_data = {
            "content": "This is a test comment.",
            "post_id": sample_post.id
        }

        # Authenticate as sample user
        client.headers.update({"Authorization": f"Bearer {sample_user.id}"})

        response = client.post("/api/v1/comments/", json=comment_data)

        assert response.status_code == 201
        data = response.json()
        assert data["content"] == comment_data["content"]
        assert data["post_id"] == sample_post.id
        assert data["author_id"] == sample_user.id

    def test_get_comments_for_post(self, client, sample_post, sample_comment):
        """Test retrieving comments for a post."""
        response = client.get(f"/api/v1/posts/{sample_post.id}/comments")

        assert response.status_code == 200
        data = response.json()
        assert len(data) >= 1
        assert any(comment["id"] == sample_comment.id for comment in data)

class TestAPIIntegration:
    """Integration tests for API endpoints."""

    def test_user_post_relationship(self, client, sample_user, sample_post):
        """Test user-post relationship."""
        response = client.get(f"/api/v1/users/{sample_user.id}/posts")

        assert response.status_code == 200
        data = response.json()
        assert len(data) >= 1
        assert any(post["id"] == sample_post.id for post in data)

    def test_post_comment_relationship(self, client, sample_post, sample_comment):
        """Test post-comment relationship."""
        response = client.get(f"/api/v1/posts/{sample_post.id}/comments")

        assert response.status_code == 200
        data = response.json()
        assert len(data) >= 1
        assert any(comment["id"] == sample_comment.id for comment in data)

    def test_search_functionality(self, client, sample_post):
        """Test search functionality."""
        response = client.get(f"/api/v1/search?q={sample_post.title}")

        assert response.status_code == 200
        data = response.json()
        assert "posts" in data
        assert "users" in data
        assert len(data["posts"]) >= 1

class TestDatabaseIntegration:
    """Integration tests for database operations."""

    def test_transaction_rollback_on_error(self, client, sample_user):
        """Test transaction rollback on error."""
        # This test would verify that database transactions are properly
        # rolled back when an error occurs during a complex operation
        
        post_data = {
            "title": "Valid Post",
            "content": "Valid content",
            "is_published": True
        }

        # Simulate a database error during post creation
        with pytest.raises(Exception):
            # This would be implemented with actual database error simulation
            pass

        # Verify that no partial data was saved
        response = client.get("/api/v1/posts/")
        data = response.json()
        assert not any(post["title"] == "Valid Post" for post in data)

    def test_concurrent_requests(self, async_client, sample_user):
        """Test handling of concurrent requests."""
        import asyncio

        async def create_post(title):
            post_data = {
                "title": title,
                "content": f"Content for {title}",
                "is_published": True
            }
            response = await async_client.post(
                "/api/v1/posts/",
                json=post_data,
                headers={"Authorization": f"Bearer {sample_user.id}"}
            )
            return response

        # Create multiple concurrent requests
        tasks = [
            create_post(f"Concurrent Post {i}")
            for i in range(10)
        ]

        responses = await asyncio.gather(*tasks)

        # Verify all requests succeeded
        for response in responses:
            assert response.status_code == 201

        # Verify all posts were created
        response = await async_client.get("/api/v1/posts/")
        data = response.json()
        concurrent_posts = [
            post for post in data
            if post["title"].startswith("Concurrent Post")
        ]
        assert len(concurrent_posts) == 10

class TestAuthenticationIntegration:
    """Integration tests for authentication."""

    def test_login_success(self, client, sample_user):
        """Test successful login."""
        login_data = {
            "username": sample_user.username,
            "password": "testpassword"  # This would be the actual password
        }

        response = client.post("/api/v1/auth/login", json=login_data)

        assert response.status_code == 200
        data = response.json()
        assert "access_token" in data
        assert "token_type" in data
        assert data["token_type"] == "bearer"

    def test_protected_endpoint_without_token(self, client):
        """Test accessing protected endpoint without token."""
        response = client.post("/api/v1/posts/", json={
            "title": "Test Post",
            "content": "Test content"
        })

        assert response.status_code == 401
        assert "not authenticated" in response.json()["detail"].lower()

    def test_protected_endpoint_with_invalid_token(self, client):
        """Test accessing protected endpoint with invalid token."""
        client.headers.update({"Authorization": "Bearer invalid_token"})

        response = client.post("/api/v1/posts/", json={
            "title": "Test Post",
            "content": "Test content"
        })

        assert response.status_code == 401

# Performance Tests
class TestPerformanceIntegration:
    """Integration tests for performance."""

    def test_bulk_operations_performance(self, client, sample_user):
        """Test performance of bulk operations."""
        import time

        client.headers.update({"Authorization": f"Bearer {sample_user.id}"})

        # Measure time for creating multiple posts
        start_time = time.time()
        
        for i in range(100):
            post_data = {
                "title": f"Bulk Post {i}",
                "content": f"Bulk content {i}",
                "is_published": True
            }
            response = client.post("/api/v1/posts/", json=post_data)
            assert response.status_code == 201

        end_time = time.time()
        duration = end_time - start_time

        # Should complete within reasonable time (adjust threshold as needed)
        assert duration < 10.0, f"Bulk operations took too long: {duration}s"

    def test_large_payload_handling(self, client, sample_user):
        """Test handling of large payloads."""
        client.headers.update({"Authorization": f"Bearer {sample_user.id}"})

        # Create post with very large content
        large_content = "A" * 1000000  # 1MB of content
        
        post_data = {
            "title": "Large Content Post",
            "content": large_content,
            "is_published": True
        }

        response = client.post("/api/v1/posts/", json=post_data)

        # Should handle large payload (or return appropriate error)
        assert response.status_code in [201, 413, 422]

# Error Handling Tests
class TestErrorHandlingIntegration:
    """Integration tests for error handling."""

    def test_malformed_json_request(self, client):
        """Test handling of malformed JSON."""
        response = client.post(
            "/api/v1/users/",
            data="invalid json",
            headers={"Content-Type": "application/json"}
        )

        assert response.status_code == 422

    def test_invalid_endpoint(self, client):
        """Test handling of invalid endpoints."""
        response = client.get("/api/v1/invalid/endpoint")

        assert response.status_code == 404

    def test_method_not_allowed(self, client):
        """Test handling of disallowed HTTP methods."""
        response = client.patch("/api/v1/users/")

        assert response.status_code == 405

# Configuration Tests
class TestConfigurationIntegration:
    """Integration tests for configuration."""

    def test_cors_headers(self, client):
        """Test CORS headers are properly set."""
        response = client.options("/api/v1/users/")

        assert "access-control-allow-origin" in response.headers

    def test_rate_limiting(self, client):
        """Test rate limiting is working."""
        # Make multiple rapid requests
        responses = []
        for i in range(100):
            response = client.get("/api/v1/users/")
            responses.append(response)
            if response.status_code == 429:
                break

        # Should eventually hit rate limit
        rate_limited = any(r.status_code == 429 for r in responses)
        assert rate_limited, "Rate limiting not working"

# Test utilities
@pytest.fixture
def auth_headers(sample_user):
    """Create authentication headers for testing."""
    return {"Authorization": f"Bearer {sample_user.id}"}

@pytest.fixture
def create_multiple_posts(db_session, sample_user):
    """Create multiple posts for testing."""
    posts = []
    for i in range(10):
        post = Post(
            title=f"Test Post {i}",
            content=f"Content for test post {i}",
            author_id=sample_user.id,
            is_published=True
        )
        db_session.add(post)
        posts.append(post)
    db_session.commit()
    return posts

# Custom assertions
def assert_valid_user_response(response_data):
    """Assert that user response data is valid."""
    required_fields = ["id", "username", "email", "is_active"]
    for field in required_fields:
        assert field in response_data, f"Missing required field: {field}"
    assert "hashed_password" not in response_data, "Password should not be returned"

def assert_valid_post_response(response_data):
    """Assert that post response data is valid."""
    required_fields = ["id", "title", "content", "author_id", "created_at"]
    for field in required_fields:
        assert field in response_data, f"Missing required field: {field}"
    assert "author" in response_data, "Author information should be included"

3. End-to-End тесты с Cypress

// cypress/e2e/user-journey.cy.js
describe('User Journey E2E Tests', () => {
  beforeEach(() => {
    // Clear cookies and localStorage before each test
    cy.clearCookies();
    cy.clearLocalStorage();
    
    // Visit the application
    cy.visit('/');
  });

  describe('Registration and Login Flow', () => {
    it('should allow user to register and login successfully', () => {
      const userData = {
        username: 'testuser',
        email: 'test@example.com',
        password: 'SecurePassword123!'
      };

      // Navigate to registration page
      cy.get('[data-testid="nav-register"]').click();
      cy.url().should('include', '/register');

      // Fill registration form
      cy.get('[data-testid="username-input"]').type(userData.username);
      cy.get('[data-testid="email-input"]').type(userData.email);
      cy.get('[data-testid="password-input"]').type(userData.password);
      cy.get('[data-testid="confirm-password-input"]').type(userData.password);

      // Submit registration
      cy.get('[data-testid="register-button"]').click();

      // Should redirect to login page
      cy.url().should('include', '/login');
      
      // Should show success message
      cy.get('[data-testid="success-message"]')
        .should('be.visible')
        .and('contain', 'Registration successful');

      // Login with new credentials
      cy.get('[data-testid="username-input"]').type(userData.username);
      cy.get('[data-testid="password-input"]').type(userData.password);
      cy.get('[data-testid="login-button"]').click();

      // Should redirect to dashboard
      cy.url().should('include', '/dashboard');
      
      // Should show user information
      cy.get('[data-testid="user-menu"]').should('contain', userData.username);
    });

    it('should show validation errors for invalid registration', () => {
      // Navigate to registration page
      cy.get('[data-testid="nav-register"]').click();

      // Submit empty form
      cy.get('[data-testid="register-button"]').click();

      // Should show validation errors
      cy.get('[data-testid="username-error"]').should('be.visible');
      cy.get('[data-testid="email-error"]').should('be.visible');
      cy.get('[data-testid="password-error"]').should('be.visible');

      // Try with invalid email
      cy.get('[data-testid="email-input"]').type('invalid-email');
      cy.get('[data-testid="register-button"]').click();
      
      cy.get('[data-testid="email-error"]')
        .should('be.visible')
        .and('contain', 'valid email');

      // Try with weak password
      cy.get('[data-testid="username-input"]').type('testuser');
      cy.get('[data-testid="password-input"]').type('123');
      cy.get('[data-testid="register-button"]').click();
      
      cy.get('[data-testid="password-error"]')
        .should('be.visible')
        .and('contain', 'at least 8 characters');
    });
  });

  describe('Post Creation and Management', () => {
    beforeEach(() => {
      // Login before each test
      cy.login('testuser', 'SecurePassword123!');
    });

    it('should create, edit, and delete a post successfully', () => {
      const postData = {
        title: 'My Test Post',
        content: 'This is the content of my test post. It should be long enough to be valid.',
        tags: ['testing', 'cypress', 'e2e']
      };

      // Navigate to create post page
      cy.get('[data-testid="nav-create-post"]').click();
      cy.url().should('include', '/posts/create');

      // Fill post form
      cy.get('[data-testid="title-input"]').type(postData.title);
      cy.get('[data-testid="content-textarea"]').type(postData.content);
      
      // Add tags
      postData.tags.forEach(tag => {
        cy.get('[data-testid="tag-input"]').type(`${tag}{enter}`);
      });

      // Submit form
      cy.get('[data-testid="create-post-button"]').click();

      // Should redirect to post detail page
      cy.url().should('match', /\/posts\/\d+/);

      // Verify post content
      cy.get('[data-testid="post-title"]').should('contain', postData.title);
      cy.get('[data-testid="post-content"]').should('contain', postData.content);
      
      // Verify tags
      postData.tags.forEach(tag => {
        cy.get('[data-testid="post-tags"]').should('contain', tag);
      });

      // Edit the post
      cy.get('[data-testid="edit-post-button"]').click();
      cy.url().should('match', /\/posts\/\d+\/edit/);

      const updatedTitle = 'Updated Test Post';
      cy.get('[data-testid="title-input"]').clear().type(updatedTitle);
      cy.get('[data-testid="update-post-button"]').click();

      // Verify updated content
      cy.get('[data-testid="post-title"]').should('contain', updatedTitle);

      // Add a comment
      const commentText = 'This is a test comment';
      cy.get('[data-testid="comment-input"]').type(commentText);
      cy.get('[data-testid="add-comment-button"]').click();

      // Verify comment appears
      cy.get('[data-testid="comments-section"]')
        .should('contain', commentText);

      // Like the post
      cy.get('[data-testid="like-button"]').click();
      cy.get('[data-testid="like-button"]')
        .should('have.class', 'liked')
        .and('contain', '1');

      // Unlike the post
      cy.get('[data-testid="like-button"]').click();
      cy.get('[data-testid="like-button"]')
        .should('not.have.class', 'liked')
        .and('contain', '0');

      // Delete the post
      cy.get('[data-testid="delete-post-button"]').click();
      cy.get('[data-testid="confirm-delete"]').click();

      // Should redirect to posts list
      cy.url().should('include', '/posts');
      
      // Verify post is no longer in list
      cy.get('[data-testid="posts-list"]').should('not.contain', postData.title);
    });

    it('should search and filter posts', () => {
      // Create multiple posts with different content
      const posts = [
        { title: 'JavaScript Tutorial', content: 'Learn JavaScript programming', tags: ['javascript', 'tutorial'] },
        { title: 'Python Guide', content: 'Python programming guide', tags: ['python', 'guide'] },
        { title: 'React Patterns', content: 'Common React patterns', tags: ['react', 'patterns'] }
      ];

      posts.forEach(post => {
        cy.createPost(post.title, post.content, post.tags);
      });

      // Navigate to posts list
      cy.get('[data-testid="nav-posts"]').click();
      cy.url().should('include', '/posts');

      // Test search functionality
      cy.get('[data-testid="search-input"]').type('JavaScript');
      cy.get('[data-testid="search-button"]').click();

      // Should only show JavaScript post
      cy.get('[data-testid="posts-list"]')
        .should('contain', 'JavaScript Tutorial')
        .and('not.contain', 'Python Guide')
        .and('not.contain', 'React Patterns');

      // Clear search
      cy.get('[data-testid="clear-search"]').click();

      // Test tag filtering
      cy.get('[data-testid="tag-filter"]').select('javascript');
      
      cy.get('[data-testid="posts-list"]')
        .should('contain', 'JavaScript Tutorial')
        .and('not.contain', 'Python Guide');

      // Test sorting
      cy.get('[data-testid="sort-select"]').select('title');
      
      // Posts should be sorted alphabetically
      cy.get('[data-testid="posts-list"] [data-testid="post-title"]')
        .then($titles => {
          const titles = $titles.map((i, el) => Cypress.$(el).text()).get();
          const sortedTitles = [...titles].sort();
          expect(titles).to.deep.equal(sortedTitles);
        });
    });
  });

  describe('User Profile Management', () => {
    beforeEach(() => {
      cy.login('testuser', 'SecurePassword123!');
    });

    it('should update user profile successfully', () => {
      const profileData = {
        firstName: 'John',
        lastName: 'Doe',
        bio: 'Software developer passionate about testing',
        location: 'San Francisco, CA'
      };

      // Navigate to profile page
      cy.get('[data-testid="nav-profile"]').click();
      cy.url().should('include', '/profile');

      // Edit profile
      cy.get('[data-testid="edit-profile-button"]').click();

      // Fill profile form
      cy.get('[data-testid="first-name-input"]').type(profileData.firstName);
      cy.get('[data-testid="last-name-input"]').type(profileData.lastName);
      cy.get('[data-testid="bio-textarea"]').type(profileData.bio);
      cy.get('[data-testid="location-input"]').type(profileData.location);

      // Upload profile picture
      cy.get('[data-testid="avatar-upload"]').attachFile('fixtures/avatar.jpg');

      // Save profile
      cy.get('[data-testid="save-profile-button"]').click();

      // Verify profile updates
      cy.get('[data-testid="profile-display"]').should('contain', profileData.firstName);
      cy.get('[data-testid="profile-display"]').should('contain', profileData.lastName);
      cy.get('[data-testid="profile-display"]').should('contain', profileData.bio);
      cy.get('[data-testid="profile-display"]').should('contain', profileData.location);

      // Verify avatar is uploaded
      cy.get('[data-testid="profile-avatar"]')
        .should('have.attr', 'src')
        .and('match', /avatar/);
    });

    it('should change password successfully', () => {
      const newPassword = 'NewSecurePassword456!';

      // Navigate to settings
      cy.get('[data-testid="nav-settings"]').click();
      cy.url().should('include', '/settings');

      // Change password
      cy.get('[data-testid="current-password-input"]').type('SecurePassword123!');
      cy.get('[data-testid="new-password-input"]').type(newPassword);
      cy.get('[data-testid="confirm-new-password-input"]').type(newPassword);

      cy.get('[data-testid="change-password-button"]').click();

      // Should show success message
      cy.get('[data-testid="success-message"]')
        .should('be.visible')
        .and('contain', 'Password changed successfully');

      // Logout and login with new password
      cy.get('[data-testid="nav-logout"]').click();
      cy.url().should('include', '/login');

      cy.get('[data-testid="username-input"]').type('testuser');
      cy.get('[data-testid="password-input"]').type(newPassword);
      cy.get('[data-testid="login-button"]').click();

      // Should login successfully
      cy.url().should('include', '/dashboard');
    });
  });

  describe('Responsive Design', () => {
    it('should work on mobile devices', () => {
      // Set mobile viewport
      cy.viewport('iphone-x');

      // Test navigation menu
      cy.get('[data-testid="mobile-menu-toggle"]').should('be.visible').click();
      cy.get('[data-testid="mobile-menu"]').should('be.visible');

      // Test responsive layout
      cy.visit('/posts');
      cy.get('[data-testid="posts-grid"]').should('be.visible');
      
      // Posts should be in single column on mobile
      cy.get('[data-testid="posts-grid"]')
        .should('have.css', 'grid-template-columns')
        .and('match', /1fr/);

      // Test mobile forms
      cy.get('[data-testid="nav-create-post"]').click();
      cy.get('[data-testid="mobile-form"]').should('be.visible');
      
      // Form should be full width on mobile
      cy.get('[data-testid="create-post-form"]')
        .should('have.css', 'width')
        .and('match', /100%/);
    });

    it('should work on tablet devices', () => {
      // Set tablet viewport
      cy.viewport('ipad-2');

      // Test tablet layout
      cy.visit('/posts');
      cy.get('[data-testid="posts-grid"]').should('be.visible');
      
      // Posts should be in two columns on tablet
      cy.get('[data-testid="posts-grid"]')
        .should('have.css', 'grid-template-columns')
        .and('match', /1fr.*1fr/);
    });
  });

  describe('Performance Tests', () => {
    it('should load pages within acceptable time limits', () => {
      const pages = [
        '/',
        '/posts',
        '/dashboard',
        '/profile'
      ];

      pages.forEach(page => {
        cy.visit(page);
        
        // Check page load time
        cy.window().then((win) => {
          const loadTime = win.performance.timing.loadEventEnd - win.performance.timing.navigationStart;
          expect(loadTime).to.be.lessThan(3000); // 3 seconds
        });
      });
    });

    it('should handle large lists efficiently', () => {
      // Create many posts
      for (let i = 0; i < 100; i++) {
        cy.createPost(`Post ${i}`, `Content for post ${i}`, ['test']);
      }

      cy.visit('/posts');
      
      // Should implement pagination or virtual scrolling
      cy.get('[data-testid="posts-list"]').should('be.visible');
      
      // Should not load all 100 posts at once
      cy.get('[data-testid="post-item"]').should('have.length.lessThan', 50);
      
      // Test pagination
      cy.get('[data-testid="pagination"]').should('be.visible');
      cy.get('[data-testid="next-page"]').click();
      cy.url().should('include', 'page=2');
    });
  });

  describe('Accessibility Tests', () => {
    it('should be accessible to keyboard users', () => {
      // Test keyboard navigation
      cy.get('body').tab();
      cy.focused().should('have.attr', 'data-testid', 'skip-to-content');

      // Navigate through main navigation
      cy.get('[data-testid="nav-home"]').focus();
      cy.focused().should('have.attr', 'data-testid', 'nav-home');

      // Test form accessibility
      cy.visit('/register');
      cy.get('[data-testid="username-input"]').focus();
      cy.focused().should('have.attr', 'data-testid', 'username-input');

      // Check for proper labels
      cy.get('[data-testid="username-input"]').should('have.attr', 'aria-label');
      cy.get('[data-testid="password-input"]').should('have.attr', 'aria-label');
    });

    it('should have proper ARIA attributes', () => {
      cy.visit('/posts');
      
      // Check for proper ARIA roles
      cy.get('[data-testid="main-content"]').should('have.attr', 'role', 'main');
      cy.get('[data-testid="navigation"]').should('have.attr', 'role', 'navigation');
      
      // Check for ARIA labels
      cy.get('[data-testid="search-input"]').should('have.attr', 'aria-label', 'Search posts');
      cy.get('[data-testid="search-button"]').should('have.attr', 'aria-label', 'Search');
      
      // Check for ARIA descriptions
      cy.get('[data-testid="form-help"]').should('have.attr', 'aria-describedby');
    });
  });

  describe('Error Handling', () => {
    it('should handle network errors gracefully', () => {
      // Simulate network failure
      cy.intercept('POST', '/api/v1/posts', { forceNetworkError: true });
      
      cy.login('testuser', 'SecurePassword123!');
      cy.visit('/posts/create');
      
      // Fill and submit form
      cy.get('[data-testid="title-input"]').type('Test Post');
      cy.get('[data-testid="content-textarea"]').type('Test content');
      cy.get('[data-testid="create-post-button"]').click();
      
      // Should show error message
      cy.get('[data-testid="error-message"]')
        .should('be.visible')
        .and('contain', 'Network error');
    });

    it('should handle 404 errors', () => {
      cy.visit('/non-existent-page', { failOnStatusCode: false });
      
      // Should show 404 page
      cy.get('[data-testid="error-page"]').should('be.visible');
      cy.get('[data-testid="error-code"]').should('contain', '404');
      cy.get('[data-testid="error-message"]').should('contain', 'Page not found');
    });

    it('should handle server errors', () => {
      cy.intercept('GET', '/api/v1/posts', { statusCode: 500 });
      
      cy.visit('/posts');
      
      // Should show server error message
      cy.get('[data-testid="error-message"]')
        .should('be.visible')
        .and('contain', 'Server error');
    });
  });
});

// Custom commands for reusable actions
Cypress.Commands.add('login', (username, password) => {
  cy.visit('/login');
  cy.get('[data-testid="username-input"]').type(username);
  cy.get('[data-testid="password-input"]').type(password);
  cy.get('[data-testid="login-button"]').click();
  cy.url().should('include', '/dashboard');
});

Cypress.Commands.add('createPost', (title, content, tags = []) => {
  cy.visit('/posts/create');
  cy.get('[data-testid="title-input"]').type(title);
  cy.get('[data-testid="content-textarea"]').type(content);
  
  tags.forEach(tag => {
    cy.get('[data-testid="tag-input"]').type(`${tag}{enter}`);
  });
  
  cy.get('[data-testid="create-post-button"]').click();
  cy.url().should('match', /\/posts\/\d+/);
});

// cypress.config.js
module.exports = {
  e2e: {
    baseUrl: 'http://localhost:3000',
    supportFile: 'cypress/support/e2e.js',
    specPattern: 'cypress/e2e/**/*.cy.js',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
    defaultCommandTimeout: 10000,
    requestTimeout: 10000,
    responseTimeout: 10000,
    pageLoadTimeout: 30000,
    env: {
      username: 'testuser',
      password: 'SecurePassword123!'
    },
    setupNodeEvents(on, config) {
      // Custom task to clear test database
      on('task', {
        clearTestDb() {
          // Implementation to clear test database
          return null;
        },
        seedTestDb() {
          // Implementation to seed test database
          return null;
        }
      });
    }
  }
};

Раздел о E2E тестировании обычно упускается при построении приложения, однако это критически важная часть стратегии контроля качества. Cypress предоставляет мощный инструмент для автоматизации пользовательских сценариев, начиная от регистрации и заканчивая проверкой производительности.

Давайте разберемся с основными группами тестов в примере выше.

Тестирование регистрации и входа

Первая группа проверяет полный цикл регистрации и последующего входа в приложение. Тест использует селекторы data-testid для выбора элементов и проверяет несколько важных аспектов, включая валидацию формы, обработку ошибок и редирект после успешного входа. Второй тест в этой группе проверяет, что неверные данные приводят к отображению соответствующих сообщений об ошибках.

Управление постами

Это сложный набор тестов, охватывающий создание, редактирование, удаление постов, а также поиск и фильтрацию. Заметьте, что перед каждым тестом в этой группе выполняется вход в систему с помощью пользовательской команды cy.login(). Это экономит время и делает тесты более читаемыми.

Тест поиска и фильтрации демонстрирует интересный паттерн: создание нескольких постов для настройки состояния приложения перед тестированием функций поиска. Затем проверяется, что поиск по названию работает корректно, фильтрация по тегам работает правильно, и сортировка расставляет посты в нужном порядке.

Управление профилем

Эти тесты проверяют обновление информации профиля и смену пароля. Последний тест особенно интересен, так как проверяет, что после успешной смены пароля и выхода из системы пользователь может снова войти с новым паролем.

Адаптивный дизайн

Cypress позволяет протестировать приложение на разных размерах экрана. Команда cy.viewport() устанавливает размер окна браузера, а затем можно проверить, что макет корректно адаптируется для мобильных и планшетных устройств.

Производительность

Эти тесты проверяют время загрузки страниц и эффективность обработки больших списков. Первый тест измеряет время от начала навигации до полной загрузки страницы. Второй создает сто постов и проверяет, что приложение реализует пагинацию или виртуальную прокрутку, чтобы не загружать все элементы сразу.

Доступность

Эти тесты необходимо запускать, чтобы убедиться, что приложение пригодно для использования клавиатурой и имеет правильные ARIA атрибуты для использования вспомогательными технологиями.

Обработка ошибок

Последняя группа проверяет, что приложение корректно обрабатывает сетевые ошибки, ошибки 404 и ошибки сервера.

Пользовательские команды

В нижней части примера определены пользовательские Cypress команды login() и createPost(). Это позволяет повторно использовать общие действия без дублирования кода в тестах.

Конфигурация Cypress

Файл конфигурации устанавливает базовый URL, пути к файлам, размеры viewport по умолчанию и другие параметры выполнения. Интересная часть в setupNodeEvents показывает, как можно добавить пользовательские задачи, например, для очистки и инициализации тестовой базы данных.

Этот подход к E2E тестированию обеспечивает высокий уровень уверенности в том, что приложение работает так, как ожидается, с точки зрения конечного пользователя.

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

Уровни тестирования

graph TD
    A[E2E Tests] --> B[Integration Tests]
    B --> C[Unit Tests]
    
    A1[<br/>Schnelle Tests<br/>Hohe Abdeckung<br/>Günstig] --> C
    B1[<br/>Mittel<br/>Mittel<br/>Mittel] --> B
    C1[<br/>Langsame Tests<br/>Geringe Abdeckung<br/>Teuer] --> A
    
    style A fill:#ff9999
    style B fill:#ffcc99
    style C fill:#99ff99

Сравнение фреймворков для тестирования

Фреймворки для JavaScript

ФреймворкТипОсобенностиОбласть применения
JestUnit/IntegrationAll-in-One, Snapshot TestingReact/Node.js
MochaUnitГибкий, Chai AssertionNode.js/Browser
CypressE2EAll-in-One, Time TravelВеб-приложения
PlaywrightE2EMulti-Browser, ParallelВеб-приложения

Фреймворки для Python

ФреймворкТипОсобенностиОбласть применения
PyTestUnit/IntegrationFixtures, Parametrized TestsPython-приложения
UnittestUnitBuilt-in, xUnit StylePython Standard Library
SeleniumE2EАвтоматизация браузераВеб-приложения
Robot FrameworkE2EKeyword-DrivenАвтоматизация тестов

TDD vs. BDD

Test-Driven Development (TDD)

graph LR
    A[Red] --> B[Green]
    B --> C[Refactor]
    C --> A
    
    A1[Schreiter fehlenden Test] --> A
    B1[Implementiere minimalen Code] --> B
    C1[Verbessere Code-Qualität] --> C

Behavior-Driven Development (BDD)

Feature: User Registration
  As a new user
  I want to register an account
  So that I can access the application

  Scenario: Successful registration
    Given I am on the registration page
    When I fill in valid registration details
    And I submit the registration form
    Then I should see a success message
    And I should receive a confirmation email
    And my account should be created

Преимущества и недостатки

Преимущества тестирования ПО

  • Гарантия качества: ранее обнаружение ошибок
  • Защита от регрессии: автоматизированный набор тестов
  • Документация: тесты как живая документация
  • Безопасность при рефакторинге: уверенность при изменении кода
  • Снижение затрат: меньше расходов на исправление ошибок

Недостатки

  • Затраты времени: разработка тестов требует времени
  • Поддержка: тесты нужно обновлять вместе с кодом
  • Сложность: сложная подготовка тестов
  • Ложные срабатывания: тесты могут падать без причины
  • Кривая обучения: нужно освоить фреймворки тестирования

Часто задаваемые вопросы на собеседовании

  1. Чем отличаются unit-тесты от интеграционных тестов? Unit-тесты проверяют изолированные компоненты, интеграционные тесты проверяют взаимодействие нескольких компонентов.

  2. Объясните цикл Red-Green-Refactor в TDD! Red: написать падающий тест, Green: реализовать минимальный код, Refactor: улучшить качество кода.

  3. Когда используются E2E-тесты? E2E-тесты используются для проверки полных пользовательских сценариев и валидации всей системы.

  4. Какие преимущества дает BDD? BDD улучшает коммуникацию между разработчиками и заинтересованными лицами, создавая читаемые и понятные тесты.

Следующий шаг в обучении тестированию ПО

Далее в этом курсе рассмотрим Пирамида тестирования: Unit, Integration и E2E тесты в правильных пропорциях — как правильно балансировать различные уровни тестирования.

Основные источники

  1. https://jestjs.io/
  2. https://docs.pytest.org/
  3. https://www.cypress.io/
  4. https://cucumber.io/
Назад к блогу
Share:

Похожие статьи