← Назад к вопросам

Что нужно тестировать в первую очередь при написании приложения по DDD?

2.3 Middle🔥 141 комментариев
#Микросервисы и архитектура#Тестирование

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Стратегия тестирования в 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
}

Что тестировать: Все возможные сценарии удовлетворения или неудовлетворения правилу.

Практические принципы организации тестов

  1. Изоляция от инфраструктуры: Первоочередные тесты не должны касаться базы данных, HTTP-запросов или внешних сервисов. Используйте паттерн Repository с интерфейсами для абстрагирования.
  2. Использование моков: Для зависимостей (репозиториев, других сервисов) в юнит-тестах применяйте моки.
  3. Тестирование через публичный API: Тестируйте агрегаты только через их публичные методы, не нарушая инкапсуляцию.
  4. Акцент на поведении (BDD-style): Названия тестов должны отражать поведение и бизнес-правила: TestOrder_ShouldNotAddItemWhenConfirmed, TestMoneyAddition_FailsWhenCurrenciesDiffer.
  5. Покрытие "несчастливых" путей: Обязательно тестируйте ошибочные сценарии и проверку инвариантов.

Что идёт во вторую очередь

После уверенности в корректности доменного слоя внимание переключается на:

  • Интеграционные тесты для репозиториев (проверка работы с реальной БД).
  • Тестирование прикладного слоя (Application Services): Проверка корректности оркестровки вызовов доменного слоя, инфраструктуры и обработки транзакций.
  • Тестирование контроллеров/API (Presentation Layer): Проверка маппинга DTO, валидации входящих запросов.

Итог: В DDD фокус первоочередного тестирования смещён с "проверки, что код работает" на "проверку, что бизнес-правила и инварианты соблюдаются неукоснительно". Начав с тщательного юнит-тестирования сущностей, объектов-значений, агрегатов и доменных служб, вы создаёте надёжный и легко проверяемый фундамент для всего приложения. Ошибки, пойманные на этом уровне, — самые дешёвые и критически важные для бизнеса.

Что нужно тестировать в первую очередь при написании приложения по DDD? | PrepBro