Комментарии 9
Код было сложно тестировать из-за зависимости от слоя доступа к данным. Последний сложно мокать в тестах. Можно замокать работу Kafka или Redis, а вот с транзакциями баз данных это уже проблематично.
В чем была сложность тестирования слоя представления данных? go-sqlmock кмк, вполне годный для этой цели пакет. Мне он показался несколько избыточным, реализовал для себя вот такой интерфейс, т.к. работаем с sqlx пакетом:
// Sqlable -- для реализации оберток базовых sql, sqlx функций ExecContext(), GetContext(), SelectContext() @see internal/tests/mock_data.go
type Sqlable interface {
// ExecContext -- имитация sql.ExecContext()! @see internal/tests/mock_data.go: возвращает поле .RetExec
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
// SelectContext -- имитация sqlx.SelectContext()! @see internal/tests/mock_data.go: возвращает поле .RetSelect
SelectContext(ctx context.Context, dest any, query string, args ...interface{}) error
// GetContext -- имитация sqlx.GetContext()! @see internal/tests/mock_data.go: возвращает поле .RetGet
GetContext(ctx context.Context, dest any, query string, args ...interface{}) error
}
и тестирование слоя данных стало тривиальным даже для относительно сложных случаев, когда запрос на выборку далее мапится в структуры для бизнес логики.
Реализация проверяет: а) совпадение набора параметров тексту запроса; б) текст запроса его регекспу в тесте; в) тип возвращаемого результата и данные из блока SELECT, если там нет .* Возвращаемые наборы - слайсы, позволяют мокать больше одного запроса в тестируемой функции работы с набором.
Можно расширить на контроль корректности транзакций при желании, пока не было нужды в этом. Интерфейсный подход в GO тем и замечателен, что позволяет вытворять вот такие шутки. ;)
Владимир, спасибо за вопрос. Cложность как раз заключалась в тестировании отката транзакций. Также мы использовали mongodb, и тот mongodb golang client, который мы юзали, он не реализовывал sql/driver
Ну .. там же не так сложно накатать моккер для транзакций с откатом или приемом .. А для Монго писал подобный моккер для тех же самых целей. Там нет ничего сложного
// MongoCollectable is an object that implements FindOne and Find.
type MongoCollectable interface {
Find(context.Context, interface{}, ...*options.FindOptions) (*mongo.Cursor, error)
}
ИМХО
Нужно с осторожностью подходить к юнитам на слое доступа к данным. Это лишнее время и кодовая база, которую нужно поддерживать. Все становится еще сложнее, если нужно сменить СУБД или библиотеку для работы с ней.
Изоляция бизнес-логики от слоя доступа к данным, конечно, не решает всех проблем, но позволяет сфокусироваться на тестировании этой самой бизнес-логики (в которой зарыт смысл разработки всего приложения). Ну и моки можно уже не писать ручками, а взять какой-нибудь minimock и генерить по интерфейсу репозиториев доступа к данным.
Нет тесной связности бизнес-логики и инфраструктуры: если мы захотим перейти на другую базу, то это не составит труда, так как мы работаем с инфраструктурным слоем через интерфейсы.
Нет тесной связности бизнес-логики и инфраструктуры: если мы захотим перейти на другую базу, то это не составит труда, так как мы работаем с инфраструктурным слоем через интерфейсы.
Два раза - это для закрепления материала? :)
Извечный вопрос. Когда относить общение с миром в delivery (у вас controllers), а когда в repository?
В controllers заносим то, что влетает в приложение (use case). В repository - исходящий трафик. Если говорить про потоки данных, то controllers это handlers -> use case, repository это use case -> adapter
Как мы наводили порядок в проекте с помощью принципов чистой архитектуры