Архитектура приложения
Введение
Архитектура приложения определяет, как организованы компоненты системы и как они взаимодействуют друг с другом. Понимание архитектуры критически важно для тестировщика.
Зачем тестировщику знать архитектуру:
- Планировать стратегию тестирования для разных слоев приложения
- Понимать, где искать проблемы при возникновении багов
- Эффективно тестировать интеграции между компонентами
- Планировать тестирование производительности и масштабируемости
- Общаться с командой на техническом языке
Слоистая архитектура (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 тестов
- Эффективную коммуникацию - общий язык с архитекторами и разработчиками
Современные приложения используют сложные архитектурные паттерны, и понимание этих паттернов критически важно для качественного тестирования всей системы.