Что нужно тестировать в первую очередь при написании приложения по DDD?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Стратегия тестирования в Domain-Driven Design (DDD)
При разработке DDD-приложения тестирование должно быть многоуровневым и сфокусированным на ключевых артефактах домена. В первую очередь внимание уделяется ядру приложения — доменному слою, так как его корректность фундаментальна для всей системы. Вот ключевые элементы для первоочередного тестирования.
1. Юнит-тестирование доменных сущностей (Entities) и объектов-значений (Value Objects)
Это абсолютный приоритет. Эти объекты содержат бизнес-правила и инварианты (неизменные условия). Их необходимо тестировать изолированно, без инфраструктуры.
// Пример Value Object: Money
package domain
import (
"errors"
"fmt"
)
type Money struct {
amount float64
currency string
}
func NewMoney(amount float64, currency string) (*Money, error) {
if amount < 0 {
return nil, errors.New("amount cannot be negative")
}
if len(currency) != 3 {
return nil, errors.New("currency must be a 3-letter code")
}
return &Money{amount: amount, currency: currency}, nil
}
func (m *Money) Add(other *Money) (*Money, error) {
if m.currency != other.currency {
return nil, errors.New("currencies must match")
}
return NewMoney(m.amount+other.amount, m.currency)
}
Что тестировать:
- Корректность создания объектов (фабричные методы, конструкторы).
- Соблюдение инвариантов (например, отрицательная сумма денег недопустима).
- Логику методов, реализующих бизнес-операции (добавление денег, применение скидки).
- Для Value Objects — корректность сравнения (методы
Equals()).
2. Тестирование агрегатов (Aggregates)
Агрегат — центральная модель в DDD. Тестирование агрегатов сложнее, так как включает взаимодействие нескольких сущностей и защиту инвариантов согласованности в рамках целого.
// Пример агрегата: Order
package domain
type Order struct {
id string
customerID string
items []*OrderItem
status OrderStatus
// Инвариант: итоговая сумма заказа должна быть положительной и рассчитана корректно.
}
func (o *Order) AddItem(productID string, price *Money, quantity int) error {
// Проверка инвариантов перед изменением
if o.status != OrderStatusDraft {
return errors.New("cannot modify confirmed order")
}
// ... логика добавления
// После изменений должен поддерживаться инвариант согласованности
o.recalculateTotal()
return nil
}
func (o *Order) Confirm() error {
// Ещё один инвариант: нельзя подтвердить пустой заказ
if len(o.items) == 0 {
return errors.New("cannot confirm an empty order")
}
o.status = OrderStatusConfirmed
o.raiseDomainEvent(OrderConfirmedEvent{OrderID: o.id})
return nil
}
Что тестировать:
- Защиту инвариантов согласованности при любых операциях изменения (
AddItem,Confirm,Cancel). - Корректное возбуждение доменных событий (Domain Events).
- Правила, определяющие границы транзакционности (что можно изменить, а что нет в определённом статусе).
3. Тестирование доменных служб (Domain Services)
Доменные службы содержат бизнес-логику, которая не принадлежит естественным образом какой-либо одной сущности. Часто они координируют работу нескольких агрегатов.
package domain
type OrderService struct {
orderRepo OrderRepository
customerRepo CustomerRepository
}
func (s *OrderService) TransferOrderItems(sourceOrderID, targetOrderID string) error {
// Координация работы двух агрегатов (заказов)
sourceOrder, _ := s.orderRepo.FindByID(sourceOrderID)
targetOrder, _ := s.orderRepo.FindByID(targetOrderID)
// Сложная бизнес-логика, которую нецелесообразно размещать внутри Order
if !sourceOrder.CanTransferItems() || !targetOrder.CanAcceptItems() {
return errors.New("transfer is not allowed")
}
// ... логика переноса
s.orderRepo.Save(sourceOrder)
s.orderRepo.Save(targetOrder)
return nil
}
Что тестировать:
- Корректность алгоритмов и бизнес-правил, реализованных в службе.
- Координацию между разными агрегатами.
- В юнит-тестах зависимости (репозитории) заменяются моками (mocks) или стабами (stubs).
4. Тестирование спецификаций (Specifications) и политик (Policies)
Эти объекты инкапсулируют часто меняющиеся бизнес-правила для повторного использования (например, критерии отбора или правила скидок).
type PremiumCustomerSpecification struct{}
func (s *PremiumCustomerSpecification) IsSatisfiedBy(customer *Customer) bool {
return customer.IsActive() && customer.TotalPurchases() > 10000
}
Что тестировать: Все возможные сценарии удовлетворения или неудовлетворения правилу.
Практические принципы организации тестов
- Изоляция от инфраструктуры: Первоочередные тесты не должны касаться базы данных, HTTP-запросов или внешних сервисов. Используйте паттерн Repository с интерфейсами для абстрагирования.
- Использование моков: Для зависимостей (репозиториев, других сервисов) в юнит-тестах применяйте моки.
- Тестирование через публичный API: Тестируйте агрегаты только через их публичные методы, не нарушая инкапсуляцию.
- Акцент на поведении (BDD-style): Названия тестов должны отражать поведение и бизнес-правила:
TestOrder_ShouldNotAddItemWhenConfirmed,TestMoneyAddition_FailsWhenCurrenciesDiffer. - Покрытие "несчастливых" путей: Обязательно тестируйте ошибочные сценарии и проверку инвариантов.
Что идёт во вторую очередь
После уверенности в корректности доменного слоя внимание переключается на:
- Интеграционные тесты для репозиториев (проверка работы с реальной БД).
- Тестирование прикладного слоя (Application Services): Проверка корректности оркестровки вызовов доменного слоя, инфраструктуры и обработки транзакций.
- Тестирование контроллеров/API (Presentation Layer): Проверка маппинга DTO, валидации входящих запросов.
Итог: В DDD фокус первоочередного тестирования смещён с "проверки, что код работает" на "проверку, что бизнес-правила и инварианты соблюдаются неукоснительно". Начав с тщательного юнит-тестирования сущностей, объектов-значений, агрегатов и доменных служб, вы создаёте надёжный и легко проверяемый фундамент для всего приложения. Ошибки, пойманные на этом уровне, — самые дешёвые и критически важные для бизнеса.