Архитектура приложения

Введение

Архитектура приложения определяет, как организованы компоненты системы и как они взаимодействуют друг с другом. Понимание архитектуры критически важно для тестировщика.

Зачем тестировщику знать архитектуру:

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

Слоистая архитектура (Layered Architecture)

Классическая трехслойная архитектура

┌─────────────────────────────────┐
│     Presentation Layer          │  ← UI, Controllers, API endpoints
│     (Слой представления)        │
├─────────────────────────────────┤
│     Business Logic Layer        │  ← Бизнес-логика, валидация
│     (Слой бизнес-логики)        │
├─────────────────────────────────┤
│     Data Access Layer           │  ← Работа с БД, внешние API
│     (Слой доступа к данным)     │
└─────────────────────────────────┘

Слой представления (Presentation Layer)

Что включает:

  • Пользовательский интерфейс (UI)
  • REST API контроллеры
  • GraphQL резолверы
  • Валидация входных данных
  • Аутентификация и авторизация

Пример на Express.js:

// API контроллер
app.post('/api/users', async (req, res) => {
    try {
        // Валидация входных данных
        const { error, value } = userSchema.validate(req.body);
        if (error) {
            return res.status(400).json({ error: error.details[0].message });
        }
        
        // Вызов бизнес-логики
        const user = await userService.createUser(value);
        
        // Возврат результата
        res.status(201).json(user);
    } catch (error) {
        res.status(500).json({ error: 'Internal server error' });
    }
});

Тестирование слоя представления:

// Интеграционные тесты API
describe('Users API', () => {
    test('POST /api/users should create user', async () => {
        const userData = {
            name: 'John Doe',
            email: 'john@example.com'
        };
        
        const response = await request(app)
            .post('/api/users')
            .send(userData)
            .expect(201);
            
        expect(response.body.name).toBe(userData.name);
        expect(response.body.email).toBe(userData.email);
        expect(response.body.id).toBeDefined();
    });
    
    test('POST /api/users should validate input', async () => {
        const invalidData = {
            name: '',
            email: 'invalid-email'
        };
        
        await request(app)
            .post('/api/users')
            .send(invalidData)
            .expect(400);
    });
});

Слой бизнес-логики (Business Logic Layer)

Что включает:

  • Бизнес-правила и процессы
  • Валидацию бизнес-данных
  • Вычисления и трансформации
  • Workflow и state management

Пример сервиса:

class UserService {
    constructor(userRepository, emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
    
    async createUser(userData) {
        // Проверка бизнес-правил
        const existingUser = await this.userRepository.findByEmail(userData.email);
        if (existingUser) {
            throw new Error('User with this email already exists');
        }
        
        // Бизнес-логика создания пользователя
        const user = {
            ...userData,
            id: generateId(),
            createdAt: new Date(),
            status: 'active',
            emailVerified: false
        };
        
        // Сохранение в БД
        const savedUser = await this.userRepository.save(user);
        
        // Отправка приветственного email
        await this.emailService.sendWelcomeEmail(savedUser);
        
        return savedUser;
    }
    
    async calculateUserScore(userId) {
        const user = await this.userRepository.findById(userId);
        const orders = await this.orderRepository.findByUserId(userId);
        
        // Сложная бизнес-логика расчета
        let score = 0;
        score += user.yearsActive * 10;
        score += orders.length * 5;
        score += orders.reduce((sum, order) => sum + order.amount, 0) / 100;
        
        return Math.min(score, 1000); // Максимум 1000 баллов
    }
}

Тестирование бизнес-логики:

describe('UserService', () => {
    let userService;
    let mockUserRepository;
    let mockEmailService;
    
    beforeEach(() => {
        mockUserRepository = {
            findByEmail: jest.fn(),
            save: jest.fn(),
            findById: jest.fn()
        };
        mockEmailService = {
            sendWelcomeEmail: jest.fn()
        };
        
        userService = new UserService(mockUserRepository, mockEmailService);
    });
    
    test('should create user with valid data', async () => {
        mockUserRepository.findByEmail.mockResolvedValue(null);
        mockUserRepository.save.mockResolvedValue({ id: 1, name: 'John' });
        
        const userData = { name: 'John', email: 'john@example.com' };
        const result = await userService.createUser(userData);
        
        expect(result.id).toBeDefined();
        expect(result.status).toBe('active');
        expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalled();
    });
    
    test('should not create user with existing email', async () => {
        mockUserRepository.findByEmail.mockResolvedValue({ id: 1 });
        
        const userData = { name: 'John', email: 'john@example.com' };
        
        await expect(userService.createUser(userData))
            .rejects.toThrow('User with this email already exists');
    });
    
    test('should calculate user score correctly', async () => {
        const user = { id: 1, yearsActive: 2 };
        const orders = [
            { amount: 100 },
            { amount: 200 }
        ];
        
        mockUserRepository.findById.mockResolvedValue(user);
        jest.spyOn(userService.orderRepository, 'findByUserId')
            .mockResolvedValue(orders);
        
        const score = await userService.calculateUserScore(1);
        
        // 2 года * 10 + 2 заказа * 5 + (100+200)/100 = 20 + 10 + 3 = 33
        expect(score).toBe(33);
    });
});

Слой доступа к данным (Data Access Layer)

Что включает:

  • Работа с базами данных
  • Интеграция с внешними API
  • Кэширование данных
  • Файловые операции

Пример репозитория:

class UserRepository {
    constructor(database) {
        this.db = database;
    }
    
    async findById(id) {
        const query = 'SELECT * FROM users WHERE id = ?';
        const result = await this.db.query(query, [id]);
        return result[0] || null;
    }
    
    async findByEmail(email) {
        const query = 'SELECT * FROM users WHERE email = ?';
        const result = await this.db.query(query, [email]);
        return result[0] || null;
    }
    
    async save(user) {
        if (user.id) {
            // Обновление существующего пользователя
            const query = `
                UPDATE users 
                SET name = ?, email = ?, updated_at = NOW() 
                WHERE id = ?
            `;
            await this.db.query(query, [user.name, user.email, user.id]);
            return this.findById(user.id);
        } else {
            // Создание нового пользователя
            const query = `
                INSERT INTO users (name, email, created_at) 
                VALUES (?, ?, NOW())
            `;
            const result = await this.db.query(query, [user.name, user.email]);
            return this.findById(result.insertId);
        }
    }
    
    async findActiveUsers(limit = 10) {
        const query = `
            SELECT * FROM users 
            WHERE status = 'active' 
            ORDER BY created_at DESC 
            LIMIT ?
        `;
        return await this.db.query(query, [limit]);
    }
}

Тестирование слоя данных:

describe('UserRepository', () => {
    let userRepository;
    let mockDatabase;
    
    beforeEach(() => {
        mockDatabase = {
            query: jest.fn()
        };
        userRepository = new UserRepository(mockDatabase);
    });
    
    test('should find user by id', async () => {
        const mockUser = { id: 1, name: 'John', email: 'john@example.com' };
        mockDatabase.query.mockResolvedValue([mockUser]);
        
        const result = await userRepository.findById(1);
        
        expect(result).toEqual(mockUser);
        expect(mockDatabase.query).toHaveBeenCalledWith(
            'SELECT * FROM users WHERE id = ?',
            [1]
        );
    });
    
    test('should return null if user not found', async () => {
        mockDatabase.query.mockResolvedValue([]);
        
        const result = await userRepository.findById(999);
        
        expect(result).toBeNull();
    });
    
    test('should create new user', async () => {
        const userData = { name: 'John', email: 'john@example.com' };
        const insertResult = { insertId: 1 };
        const createdUser = { id: 1, ...userData, created_at: '2024-01-15' };
        
        mockDatabase.query
            .mockResolvedValueOnce(insertResult)  // INSERT query
            .mockResolvedValueOnce([createdUser]); // SELECT query
        
        const result = await userRepository.save(userData);
        
        expect(result).toEqual(createdUser);
        expect(mockDatabase.query).toHaveBeenCalledTimes(2);
    });
});

Архитектурные паттерны

MVC (Model-View-Controller)

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│    View     │◄───┤ Controller  │───►│    Model    │
│             │    │             │    │             │
│ - HTML      │    │ - Routing   │    │ - Data      │
│ - CSS       │    │ - Logic     │    │ - Business  │
│ - Templates │    │ - Handlers  │    │ - Rules     │
└─────────────┘    └─────────────┘    └─────────────┘

Пример MVC в Express.js:

// Model (User.js)
class User {
    constructor(data) {
        this.id = data.id;
        this.name = data.name;
        this.email = data.email;
    }
    
    static async findById(id) {
        const data = await db.query('SELECT * FROM users WHERE id = ?', [id]);
        return data[0] ? new User(data[0]) : null;
    }
    
    async save() {
        if (this.id) {
            await db.query('UPDATE users SET name = ?, email = ? WHERE id = ?', 
                [this.name, this.email, this.id]);
        } else {
            const result = await db.query('INSERT INTO users (name, email) VALUES (?, ?)', 
                [this.name, this.email]);
            this.id = result.insertId;
        }
        return this;
    }
}

// Controller (UserController.js)
class UserController {
    static async show(req, res) {
        try {
            const user = await User.findById(req.params.id);
            if (!user) {
                return res.status(404).render('error', { message: 'User not found' });
            }
            res.render('user/show', { user });
        } catch (error) {
            res.status(500).render('error', { message: 'Internal server error' });
        }
    }
    
    static async create(req, res) {
        try {
            const user = new User(req.body);
            await user.save();
            res.redirect(`/users/${user.id}`);
        } catch (error) {
            res.render('user/new', { errors: error.errors, data: req.body });
        }
    }
}

// View (user/show.hbs)
/*
<h1>{{user.name}}</h1>
<p>Email: {{user.email}}</p>
<p>ID: {{user.id}}</p>
<a href="/users/{{user.id}}/edit">Edit</a>
*/

Тестирование MVC:

// Тестирование Model
describe('User Model', () => {
    test('should find user by id', async () => {
        const user = await User.findById(1);
        expect(user).toBeInstanceOf(User);
        expect(user.id).toBe(1);
    });
    
    test('should save new user', async () => {
        const userData = { name: 'John', email: 'john@example.com' };
        const user = new User(userData);
        
        await user.save();
        
        expect(user.id).toBeDefined();
    });
});

// Тестирование Controller
describe('UserController', () => {
    test('should show user page', async () => {
        const req = { params: { id: '1' } };
        const res = {
            render: jest.fn(),
            status: jest.fn().mockReturnThis()
        };
        
        await UserController.show(req, res);
        
        expect(res.render).toHaveBeenCalledWith('user/show', 
            expect.objectContaining({ user: expect.any(User) }));
    });
});

MVP (Model-View-Presenter)

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│    View     │◄──►│  Presenter  │───►│    Model    │
│             │    │             │    │             │
│ - Passive   │    │ - All Logic │    │ - Data      │
│ - UI Events │    │ - View      │    │ - Business  │
│ - Display   │    │ - Updates   │    │ - Rules     │
└─────────────┘    └─────────────┘    └─────────────┘

Особенности MVP:

  • View максимально пассивная
  • Presenter содержит всю логику
  • Легче тестировать (можно мокать View)

MVVM (Model-View-ViewModel)

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│    View     │◄──►│ ViewModel   │───►│    Model    │
│             │    │             │    │             │
│ - Bindings  │    │ - Commands  │    │ - Data      │
│ - Templates │    │ - Binding   │    │ - Business  │
│ - Events    │    │ - Data      │    │ - Rules     │
└─────────────┘    └─────────────┘    └─────────────┘

Пример на Vue.js:

// ViewModel (Vue component)
export default {
    name: 'UserProfile',
    data() {
        return {
            user: null,
            loading: false,
            errors: []
        };
    },
    computed: {
        fullName() {
            return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';
        },
        isValid() {
            return this.errors.length === 0;
        }
    },
    methods: {
        async loadUser(userId) {
            this.loading = true;
            try {
                this.user = await UserService.findById(userId);
            } catch (error) {
                this.errors.push('Failed to load user');
            } finally {
                this.loading = false;
            }
        },
        async saveUser() {
            if (!this.isValid) return;
            
            try {
                await UserService.save(this.user);
                this.$router.push('/users');
            } catch (error) {
                this.errors.push('Failed to save user');
            }
        }
    }
};

Тестирование MVVM:

import { mount } from '@vue/test-utils';
import UserProfile from '@/components/UserProfile.vue';
import UserService from '@/services/UserService';

jest.mock('@/services/UserService');

describe('UserProfile', () => {
    test('should load user on mount', async () => {
        const mockUser = { id: 1, firstName: 'John', lastName: 'Doe' };
        UserService.findById.mockResolvedValue(mockUser);
        
        const wrapper = mount(UserProfile, {
            propsData: { userId: 1 }
        });
        
        await wrapper.vm.$nextTick();
        
        expect(wrapper.vm.user).toEqual(mockUser);
        expect(wrapper.vm.fullName).toBe('John Doe');
    });
    
    test('should handle loading state', async () => {
        UserService.findById.mockImplementation(() => 
            new Promise(resolve => setTimeout(resolve, 100)));
        
        const wrapper = mount(UserProfile, {
            propsData: { userId: 1 }
        });
        
        expect(wrapper.vm.loading).toBe(true);
        
        await new Promise(resolve => setTimeout(resolve, 150));
        
        expect(wrapper.vm.loading).toBe(false);
    });
});

Микросервисная архитектура

Основные принципы

┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│   User      │  │   Order     │  │  Payment    │
│  Service    │  │  Service    │  │  Service    │
│             │  │             │  │             │
│ - Users API │  │ - Orders    │  │ - Payments  │
│ - User DB   │  │ - Order DB  │  │ - Payment   │
│             │  │             │  │   Gateway   │
└─────────────┘  └─────────────┘  └─────────────┘
        │                │                │
        └────────────────┼────────────────┘
                         │
                ┌─────────────┐
                │ API Gateway │
                │             │
                │ - Routing   │
                │ - Auth      │
                │ - Rate      │
                │   Limiting  │
                └─────────────┘

Характеристики микросервисов:

  • Независимые развертывания
  • Собственные базы данных
  • Коммуникация через API
  • Технологическая независимость

Паттерны коммуникации

Синхронная коммуникация (HTTP/REST):

// Order Service вызывает User Service
class OrderService {
    constructor(userServiceClient) {
        this.userServiceClient = userServiceClient;
    }
    
    async createOrder(orderData) {
        // Проверяем пользователя
        const user = await this.userServiceClient.getUser(orderData.userId);
        if (!user) {
            throw new Error('User not found');
        }
        
        // Создаем заказ
        const order = {
            ...orderData,
            userEmail: user.email,
            createdAt: new Date()
        };
        
        return await this.orderRepository.save(order);
    }
}

// Тестирование межсервисной коммуникации
describe('OrderService', () => {
    test('should create order for existing user', async () => {
        const mockUserClient = {
            getUser: jest.fn().mockResolvedValue({ 
                id: 1, 
                email: 'john@example.com' 
            })
        };
        
        const orderService = new OrderService(mockUserClient);
        const orderData = { userId: 1, productId: 100, quantity: 2 };
        
        const order = await orderService.createOrder(orderData);
        
        expect(order.userEmail).toBe('john@example.com');
        expect(mockUserClient.getUser).toHaveBeenCalledWith(1);
    });
    
    test('should handle user service failure', async () => {
        const mockUserClient = {
            getUser: jest.fn().mockRejectedValue(new Error('Service unavailable'))
        };
        
        const orderService = new OrderService(mockUserClient);
        const orderData = { userId: 1, productId: 100 };
        
        await expect(orderService.createOrder(orderData))
            .rejects.toThrow('Service unavailable');
    });
});

Асинхронная коммуникация (Message Queue):

// Event-driven архитектура
class OrderService {
    constructor(eventBus, orderRepository) {
        this.eventBus = eventBus;
        this.orderRepository = orderRepository;
        
        // Подписываемся на события
        this.eventBus.subscribe('user.deleted', this.handleUserDeleted.bind(this));
        this.eventBus.subscribe('payment.completed', this.handlePaymentCompleted.bind(this));
    }
    
    async createOrder(orderData) {
        const order = await this.orderRepository.save({
            ...orderData,
            status: 'pending',
            createdAt: new Date()
        });
        
        // Отправляем событие
        await this.eventBus.publish('order.created', {
            orderId: order.id,
            userId: order.userId,
            amount: order.amount
        });
        
        return order;
    }
    
    async handleUserDeleted(event) {
        // Обрабатываем удаление пользователя
        const orders = await this.orderRepository.findByUserId(event.userId);
        
        for (const order of orders) {
            if (order.status === 'pending') {
                await this.cancelOrder(order.id);
            }
        }
    }
    
    async handlePaymentCompleted(event) {
        const order = await this.orderRepository.findById(event.orderId);
        if (order) {
            order.status = 'confirmed';
            order.paidAt = new Date();
            await this.orderRepository.save(order);
            
            await this.eventBus.publish('order.confirmed', {
                orderId: order.id,
                userId: order.userId
            });
        }
    }
}

Тестирование асинхронных событий:

describe('OrderService Events', () => {
    let orderService;
    let mockEventBus;
    let mockOrderRepository;
    
    beforeEach(() => {
        mockEventBus = {
            subscribe: jest.fn(),
            publish: jest.fn()
        };
        mockOrderRepository = {
            save: jest.fn(),
            findById: jest.fn(),
            findByUserId: jest.fn()
        };
        
        orderService = new OrderService(mockEventBus, mockOrderRepository);
    });
    
    test('should publish order.created event', async () => {
        const orderData = { userId: 1, productId: 100, amount: 50 };
        const savedOrder = { id: 1, ...orderData, status: 'pending' };
        
        mockOrderRepository.save.mockResolvedValue(savedOrder);
        
        await orderService.createOrder(orderData);
        
        expect(mockEventBus.publish).toHaveBeenCalledWith('order.created', {
            orderId: 1,
            userId: 1,
            amount: 50
        });
    });
    
    test('should handle payment completed event', async () => {
        const order = { id: 1, status: 'pending', userId: 1 };
        const event = { orderId: 1, amount: 50 };
        
        mockOrderRepository.findById.mockResolvedValue(order);
        
        await orderService.handlePaymentCompleted(event);
        
        expect(mockOrderRepository.save).toHaveBeenCalledWith(
            expect.objectContaining({
                status: 'confirmed',
                paidAt: expect.any(Date)
            })
        );
    });
});

Circuit Breaker Pattern

Защита от каскадных сбоев:

class CircuitBreaker {
    constructor(threshold = 5, timeout = 60000) {
        this.threshold = threshold;
        this.timeout = timeout;
        this.failureCount = 0;
        this.lastFailureTime = null;
        this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    }
    
    async call(fn) {
        if (this.state === 'OPEN') {
            if (Date.now() - this.lastFailureTime > this.timeout) {
                this.state = 'HALF_OPEN';
            } else {
                throw new Error('Circuit breaker is OPEN');
            }
        }
        
        try {
            const result = await fn();
            this.onSuccess();
            return result;
        } catch (error) {
            this.onFailure();
            throw error;
        }
    }
    
    onSuccess() {
        this.failureCount = 0;
        this.state = 'CLOSED';
    }
    
    onFailure() {
        this.failureCount++;
        this.lastFailureTime = Date.now();
        
        if (this.failureCount >= this.threshold) {
            this.state = 'OPEN';
        }
    }
}

// Использование Circuit Breaker
class UserServiceClient {
    constructor() {
        this.circuitBreaker = new CircuitBreaker(3, 30000);
    }
    
    async getUser(userId) {
        return await this.circuitBreaker.call(async () => {
            const response = await fetch(`/api/users/${userId}`);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }
            return await response.json();
        });
    }
}

Тестирование Circuit Breaker:

describe('Circuit Breaker', () => {
    test('should open circuit after threshold failures', async () => {
        const circuitBreaker = new CircuitBreaker(2, 1000);
        const failingFunction = jest.fn().mockRejectedValue(new Error('Service error'));
        
        // Первые два вызова - failures
        await expect(circuitBreaker.call(failingFunction)).rejects.toThrow();
        await expect(circuitBreaker.call(failingFunction)).rejects.toThrow();
        
        // Третий вызов - circuit должен быть открыт
        await expect(circuitBreaker.call(failingFunction))
            .rejects.toThrow('Circuit breaker is OPEN');
        
        expect(circuitBreaker.state).toBe('OPEN');
        expect(failingFunction).toHaveBeenCalledTimes(2); // Третий вызов не дошел до функции
    });
    
    test('should reset circuit after timeout', async () => {
        const circuitBreaker = new CircuitBreaker(1, 100);
        const failingFunction = jest.fn().mockRejectedValue(new Error('Service error'));
        
        // Открываем circuit
        await expect(circuitBreaker.call(failingFunction)).rejects.toThrow();
        expect(circuitBreaker.state).toBe('OPEN');
        
        // Ждем timeout
        await new Promise(resolve => setTimeout(resolve, 150));
        
        // Следующий вызов должен перевести в HALF_OPEN
        const successFunction = jest.fn().mockResolvedValue('success');
        const result = await circuitBreaker.call(successFunction);
        
        expect(result).toBe('success');
        expect(circuitBreaker.state).toBe('CLOSED');
    });
});

Serverless архитектура

Function as a Service (FaaS)

Характеристики:

  • Функции выполняются по требованию
  • Автоматическое масштабирование
  • Оплата только за время выполнения
  • Stateless функции

Пример AWS Lambda функции:

// handler.js
exports.handler = async (event, context) => {
    try {
        const { userId } = JSON.parse(event.body);
        
        // Валидация
        if (!userId) {
            return {
                statusCode: 400,
                body: JSON.stringify({ error: 'userId is required' })
            };
        }
        
        // Бизнес-логика
        const user = await getUserFromDatabase(userId);
        const recommendations = await generateRecommendations(user);
        
        return {
            statusCode: 200,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify({ recommendations })
        };
        
    } catch (error) {
        console.error('Error:', error);
        
        return {
            statusCode: 500,
            body: JSON.stringify({ error: 'Internal server error' })
        };
    }
};

async function getUserFromDatabase(userId) {
    // Подключение к БД и получение пользователя
    const AWS = require('aws-sdk');
    const dynamodb = new AWS.DynamoDB.DocumentClient();
    
    const params = {
        TableName: 'Users',
        Key: { userId }
    };
    
    const result = await dynamodb.get(params).promise();
    return result.Item;
}

async function generateRecommendations(user) {
    // Логика генерации рекомендаций
    return [
        'Product A',
        'Product B', 
        'Product C'
    ];
}

Тестирование Serverless функций:

const { handler } = require('./handler');

// Мокаем AWS SDK
jest.mock('aws-sdk', () => ({
    DynamoDB: {
        DocumentClient: jest.fn(() => ({
            get: jest.fn(() => ({
                promise: jest.fn()
            }))
        }))
    }
}));

describe('Lambda Handler', () => {
    test('should return recommendations for valid user', async () => {
        const mockUser = { userId: '123', preferences: ['electronics'] };
        
        // Мокаем получение пользователя из БД
        const AWS = require('aws-sdk');
        const mockGet = AWS.DynamoDB.DocumentClient().get().promise;
        mockGet.mockResolvedValue({ Item: mockUser });
        
        const event = {
            body: JSON.stringify({ userId: '123' })
        };
        
        const result = await handler(event, {});
        
        expect(result.statusCode).toBe(200);
        const body = JSON.parse(result.body);
        expect(body.recommendations).toBeDefined();
        expect(Array.isArray(body.recommendations)).toBe(true);
    });
    
    test('should return 400 for missing userId', async () => {
        const event = {
            body: JSON.stringify({})
        };
        
        const result = await handler(event, {});
        
        expect(result.statusCode).toBe(400);
        const body = JSON.parse(result.body);
        expect(body.error).toBe('userId is required');
    });
    
    test('should handle database errors', async () => {
        const AWS = require('aws-sdk');
        const mockGet = AWS.DynamoDB.DocumentClient().get().promise;
        mockGet.mockRejectedValue(new Error('Database error'));
        
        const event = {
            body: JSON.stringify({ userId: '123' })
        };
        
        const result = await handler(event, {});
        
        expect(result.statusCode).toBe(500);
        const body = JSON.parse(result.body);
        expect(body.error).toBe('Internal server error');
    });
});

Тестирование архитектуры

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

        ┌─────────────────┐
        │   E2E Tests     │  ← Малое количество, медленные
        │   (UI Tests)    │
        ├─────────────────┤
        │ Integration     │  ← Среднее количество
        │     Tests       │
        ├─────────────────┤
        │   Unit Tests    │  ← Большое количество, быстрые
        │                 │
        └─────────────────┘

Unit тесты - тестирование отдельных компонентов

// Тестирование бизнес-логики в изоляции
describe('User Service Unit Tests', () => {
    test('should validate email format', () => {
        const userService = new UserService();
        
        expect(userService.isValidEmail('test@example.com')).toBe(true);
        expect(userService.isValidEmail('invalid-email')).toBe(false);
        expect(userService.isValidEmail('')).toBe(false);
    });
    
    test('should calculate user age correctly', () => {
        const userService = new UserService();
        const birthDate = new Date('1990-01-01');
        
        const age = userService.calculateAge(birthDate);
        
        expect(age).toBeGreaterThan(30);
        expect(age).toBeLessThan(40);
    });
});

Integration тесты - тестирование взаимодействий

// Тестирование интеграции между слоями
describe('User Service Integration Tests', () => {
    let userService;
    let testDatabase;
    
    beforeAll(async () => {
        testDatabase = await createTestDatabase();
        userService = new UserService(testDatabase);
    });
    
    afterAll(async () => {
        await testDatabase.close();
    });
    
    test('should create user in database', async () => {
        const userData = {
            name: 'Integration Test User',
            email: 'integration@test.com'
        };
        
        const user = await userService.createUser(userData);
        
        expect(user.id).toBeDefined();
        
        // Проверяем, что пользователь действительно сохранен
        const savedUser = await userService.findById(user.id);
        expect(savedUser.name).toBe(userData.name);
        expect(savedUser.email).toBe(userData.email);
    });
});

E2E тесты - тестирование полных сценариев

// Тестирование полного пользовательского сценария
describe('User Registration E2E', () => {
    test('should register new user through UI', async () => {
        // Открываем страницу регистрации
        await page.goto('/register');
        
        // Заполняем форму
        await page.fill('#name', 'E2E Test User');
        await page.fill('#email', 'e2e@test.com');
        await page.fill('#password', 'securePassword123');
        await page.fill('#confirmPassword', 'securePassword123');
        
        // Отправляем форму
        await page.click('#registerButton');
        
        // Проверяем успешную регистрацию
        await page.waitForURL('/welcome');
        const welcomeMessage = await page.textContent('.welcome-message');
        expect(welcomeMessage).toContain('Welcome, E2E Test User');
        
        // Проверяем, что пользователь создан в БД
        const user = await userService.findByEmail('e2e@test.com');
        expect(user).toBeTruthy();
        expect(user.name).toBe('E2E Test User');
    });
});

Практические рекомендации

Стратегия тестирования по слоям

Presentation Layer:

□ Валидация входных данных
□ Правильные HTTP статус-коды
□ Формат ответов API
□ Обработка ошибок
□ Аутентификация и авторизация
□ Rate limiting
□ CORS настройки

Business Logic Layer:

□ Бизнес-правила и валидация
□ Вычисления и трансформации
□ Workflow и состояния
□ Граничные случаи
□ Обработка исключений
□ Performance критических операций

Data Access Layer:

□ CRUD операции
□ Транзакции БД
□ Миграции схемы
□ Производительность запросов
□ Обработка connection pool
□ Резервное копирование и восстановление

Тестирование микросервисов

Contract Testing:

// Тест контракта между сервисами
describe('User Service Contract', () => {
    test('GET /users/:id should return user schema', async () => {
        const response = await request(userService)
            .get('/users/123')
            .expect(200);
        
        // Проверяем схему ответа
        const userSchema = {
            type: 'object',
            required: ['id', 'name', 'email'],
            properties: {
                id: { type: 'number' },
                name: { type: 'string' },
                email: { type: 'string', format: 'email' }
            }
        };
        
        expect(response.body).toMatchSchema(userSchema);
    });
});

Service Mesh Testing:

// Тестирование с учетом service mesh
describe('Order Service with Service Mesh', () => {
    test('should handle circuit breaker', async () => {
        // Имитируем сбой User Service
        await mockUserService.simulateFailure();
        
        const orderData = { userId: 1, productId: 100 };
        
        // Первые запросы должны возвращать ошибки
        await expect(orderService.createOrder(orderData))
            .rejects.toThrow('User service unavailable');
        
        // После нескольких ошибок circuit breaker должен открыться
        await expect(orderService.createOrder(orderData))
            .rejects.toThrow('Circuit breaker open');
    });
});

Заключение

Понимание архитектуры приложения дает тестировщику:

  • Четкое планирование тестирования - знание того, что и на каком уровне тестировать
  • Эффективную локализацию проблем - понимание где искать баги
  • Лучшее покрытие тестами - учет всех компонентов и их взаимодействий
  • Оптимизацию стратегии тестирования - правильное соотношение unit/integration/e2e тестов
  • Эффективную коммуникацию - общий язык с архитекторами и разработчиками

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