В нашей повседневной разработке микросервисов неизбежно использование множества сторонних сервисов. Наиболее типичным из них является MySQL. Кроме того, есть и другие. ZK,Redis,Mongo,MQ, Consul, ES и т. д. Использование множества промежуточных программ также вносит определенную сложность в процесс тестирования. Если я хочу, чтобы покрытие UT моего продукта соответствовало требованиям >90%, Так что компонентно-зависимое UT — очень хлопотная штука. В большинстве случаев мы будем использовать метод пропуска, чтобы подвергнуть все тесты зависимостей промежуточного программного обеспечения процессу интеграционного тестирования, надеясь охватить тестирование использования промежуточного программного обеспечения за счет тестирования функций продукта. Конечно, когда покрытие UT не требуется, UT, ориентированное на зависимости, также должно быть ценным, и незаменимая часть процесса исследований и разработок также оставит достаточно скрытых опасностей в нашем коде.
Если нет подходящего метода UT по промежуточной цене, большинство из нас будут использовать Mock в ссылке UT. очень правильноDAOпара слоевgormиспользованияобойти, На примере MySQL мы сделаем простую демонстрацию. Полный код доступен через github.
var DB *gorm.DB
type Product struct {
Code string
Price int
}
type Repository struct {
}
func NewRepository() *Repository {
return &Repository{}
}
func OpenDB(dbUrl string) (*gorm.DB, error) {
return gorm.Open(mysql.Open(dbUrl), &gorm.Config{})
}
func (r *Repository) Select() (Product, error) {
var product Product
err := DB.First(&product, "code = ?", "D42").Error // Находить code Значение поля D42 записи
return product, err
}
func (r *Repository) Create(product Product) error {
return DB.Create(&product).Error
}
Уровень DAO использует gorm для определения общедоступной переменной DB *gorm.DB для глобального соединения MySQL. OpenDB (строка dbUrl) используется для получения соединения на основе адреса.
Create и Select используются для создания данных и запроса данных при определенных условиях соответственно.
func init() {
db, err := dao.OpenDB("")
if err != nil {
panic(err)
}
dao.DB = db
}
func QueryData() (*dao.Product, error) {
r := dao.NewRepository()
product, err := r.Select()
if err != nil {
return nil, err
}
err = DoSomethingUseProduct(product)
return &product, err
}
func DoSomethingUseProduct(product dao.Product) error {
//todo
fmt.Println(product)
return nil
}
Мы используем метод init для создания соединения с БД и инициализируем его в общедоступном соединении перед запуском программы. QueryData использует Select для запроса данных и Dosomething для выполнения некоторой бизнес-логики.
Теперь мы начинаем писать UT для QueryData, который, вероятно, должен выглядеть так. Здесь мы используем пакет Byte с открытым исходным кодом github.com/bytedance/mockey.
func TestQueryData(t *testing.T) {
mockey.PatchConvey("22", t, func() {
mockey.Mock((*dao.Repository).Select).Return(dao.Product{
Price: 1,
}, nil).Build()
defer mockey.UnPatchAll()
mockey.Mock(DoSomethingUseProduct).Return(nil).Build()
product, err := QueryData()
assert.Nil(t, err)
assert.Equal(t, 1, product.Price)
dao.DB = nil
})
}
Не имея возможности подключиться к локальной базе данных подключений, мы отдадим приоритет макетированию, чтобы обойти реальное выполнение уровня Gorm и позволить UT продолжить работу. *dao.Репозиторий).Выбрать
Выполнение метода не может быть покрыто ut. На этом этапе у некоторых старых ребят возникнет несколько вопросов.
————————————————————————————————————————————————————————
Вопрос 1. Если вы создадите MySQL локально и импортируете структуру таблицы, разве проблема не будет решена?
О: Как правило, бизнес-проекты выполняются несколькими людьми. Если А добавляет код модульного тестирования, для которого требуется локальная среда развертывания, то всем в B, C, D и т. д., кто хочет выполнить UT, необходимо будет развернуть среду. или даже инициализировать те же данные. Если проект необходимо выполнить в среде CI, также требуется среда развертывания. Код имеет плохую читаемость и низкую возможность повторного использования. Если проект также использует другое промежуточное программное обеспечение, стоимость развертывания каждого из них будет немного высока.
Вопрос 2. Уровень DAO представляет собой простую логику добавления, удаления, изменения и запроса SQL, и его не нужно тестировать с помощью ut.
О: Промежуточное ПО введено потому, что от него должна зависеть бизнес-логика. Другими словами, поскольку вы используете промежуточное программное обеспечение, такое как MySQL, и у вас должны быть сильные зависимости, когда возникает ошибка выполнения, это означает, что существует проблема с бизнес-логикой. Если это простая функция добавления, удаления, изменения и запроса, она может быть рассмотрена во время принятия функции продукта, но некоторые сложные функции продукта выполняются на основе сложных комбинаций данных. В качестве простого примера страница списка имеет 10 полей, и необходимо реализовать фильтрацию и сортировку на основе каждого поля. Код для реализации этой функции может быть следующим
func Query(condition *QueryCondition) []*Resp {
db := dao.GetDB().Select("*")
if condition.Field1 != nil {
db = db.where("Field1 = ?", condition.Field1)
}
if condition.Field2 != nil {
db = db.where("Field2 = ?", condition.Field2)
}
...(другое, если)
if condition.Field10 != nil {
db = db.where("Field10 = ?", condition.Field10)
}
.......(Другая логика сортировки страниц)
}
В этом примере, поскольку метод Query является методом нижнего уровня, на верхнем уровне может быть серия вызовов, таких как f1, f2, f3 и т. д., что в конечном итоге образует сложную логическую сеть.
Охватить все сценарии комбинирования путем принятия функций продукта может оказаться невозможным. Предполагается, что одно из условий содержит поле или синтаксическую ошибку при его написании, и оно не рассматривается во время тестирования функций продукта. После того, как он выходит в Интернет и обнаруживается пользователями во время его использования, уже слишком поздно. (Согласно описанию реального случая, синтаксическая ошибка SQL была обнаружена уже после запуска продукта, что в конечном итоге привело к серьезным потерям дохода от продукта)
————————————————————————————————————————————————————————
Здесь мы возвращаемся к теме
Mock mysql gorm Layer — это не что иное, как следующие сценарии:
кроме выбора mock data Вот только все остальное не выглядит бессмысленным, а на самом деле бессмысленно. потому что, Например, выполнение SQL в приведенном выше случае не всегда является успешным, также существует ошибка. Например, распространенные грамматические ошибки, ошибки в написании полей, формат данных, ошибки формата времени. т. д. Тогда эти ошибки можно будет обнаружить только в процессе интеграционного тестирования. На функциональных точках с несложной логикой развертывание тестовых связей и выполнение FT позволяют выявить проблемы. Однако в развитии бизнеса всегда существуют сложные логические ссылки FT, которые представляют собой тесты «черного ящика». Как мы можем гарантировать, что каждое «если» можно протестировать? Во-вторых, даже если проблема будет обнаружена в процессе FT, потребуется рабочая сила для доработки исправления и последующего его внедрения. Протестируйте еще раз, снова потерпите неудачу, снова исправьте ........ (Даже если облачная среда поддерживает быстрое развертывание, это также нарушает менталитет разработчика)
Например, для MySQL, упомянутого выше, самый простой способ — развернуть MySQL локально, а затем подключиться для тестирования, но есть несколько проблем:
Представленный сегодня артефакт Testcontainer прекрасно решает эту серию проблем.
Testcontainers Это сторонняя библиотека зависимостей с открытым исходным кодом, используемая для поддержки модульного тестирования. Обеспечивает простой и легкий API для использования Docker Реальные сервисы в контейнерах для запуска локальной разработки и тестирования зависимостей.Зависит от промежуточного программного обеспечения。Используя Тестовые контейнеры позволяют вам писать тесты, которые зависят от тех же сервисов, что и ваша производственная среда, без использования фиктивных объектов или сервисов в памяти.
Проще говоря, это просто зависимая библиотека lib, а не служба. Во-вторых, быстро создайте необходимые вам зависимые серверы с помощью Docker-контейнеров и предоставьте их для использования. Он может поддерживать все контейнеризуемые внешние зависимости, а также поддерживает несколько распространенных языков программирования и почти все часто используемое промежуточное программное обеспечение. Благодаря полному созданию контейнеров и механизму автоматической переработки нет необходимости уделять внимание переработке контейнеров во время использования.
Студенты, которые хотят узнать больше, могут посетить официальный сайт. Официальный сайт тестконтейнеров
На основе приведенного выше тестового кода мы создаем и используем TestContainer для модульного тестирования.
##demo go версия go_1.19, Соответствующий номер версии — v0.20.
##Выберите пакет модулей в соответствии с объектами, которые необходимо протестировать. Остальные можно найти в теге хранилища кода.
##https://github.com/pingcap/tidb/tree/master
go get github.com/testcontainers/testcontainers-go@v0.20.0
go get github.com/testcontainers/testcontainers-go/modules/mysql@v0.20.0
##Если нужны другие компоненты
go get github.com/testcontainers/testcontainers-go/modules/postgres@v0.20.0
Создайте файл testhelper.go для написания кода создания зависимого контейнера.
func init() {
if dao.DB != nil {
return
}
err, mysqlTestUrl := CreateTestMySQLContainer(context.Background())
if err != nil {
panic(err)
}
dao.DB, err = dao.OpenDB(mysqlTestUrl)
if err != nil {
panic(err)
}
}
func CreateTestMySQLContainer(ctx context.Context) (error, string) {
container, err := mysql.RunContainer(ctx,
testcontainers.WithImage("mysql:8.0"),
mysql.WithDatabase("test_db"),
mysql.WithUsername("root"),
mysql.WithPassword("root@123"),
//Вы также можете использовать скрипт sql для инициализации базы данных
//mysql.WithScripts(filepath.Join("..", "testdata", "init-db.sql")
)
if err != nil {
return err, ""
}
//Получаем соединение доступа
str, err := container.ConnectionString(ctx)
if err != nil {
return err, ""
}
//Распечатываем соединение, вы можете войти в систему, чтобы построить MySQL в локальной среде через соединение
log.Printf("can use this connecting string to login in db:%s", str)
return nil, str
}
//Если нужны другие зависимые контейнеры, их можно создать аналогично
//func CreateTestRedisContainer(ctx context.Context) error {}
//func CreateTestZKContainer(ctx context.Context) error {}
Мы знаем, что механизм загрузки импорта go заключается в том, чтобы сначала выполнить метод init() в импортированной зависимости, затем выполнить init в его собственном пакете, а затем выполнить вызывающий код.
Здесь мы создаем докер-контейнер mysql для инициализации через метод init и инициализируем глобальное соединение с БД. Когда UT нужно протестировать слой dao, просто импортируйте путь. Разработчикам из других команд не нужно потом уделять внимание созданию контейнеров.
func TestQueryDataUseContainer(t *testing.T) {
mockey.PatchConvey("23", t, func() {
//Инициализируем таблицы, которые необходимо протестировать. Инициализируем те таблицы, которые необходимо протестировать.
err := dao.DB.AutoMigrate(dao.Product{})
assert.Nil(t, err)
r := dao.NewRepository()
//Записываем временные тестовые данные
err = r.Create(dao.Product{
Code: "D42",
Price: 1,
})
assert.Nil(t, err)
//Выполняем тест
mockey.Mock(DoSomethingUseProduct).Return(nil).Build()
product, err := QueryData()
assert.Nil(t, err)
assert.Equal(t, 1, product.Price)
})
}
Видно, что реальные операции, связанные с mysql, действительно выполняются во время выполнения ut, так что наш код больше не нужно развертывать в специальной среде для выполнения определенного теста покрытия. Например, аналогичным образом можно протестировать зависимости промежуточного программного обеспечения, такого как Redis, MQ, Kakfa и ES.
Вопрос: Будет ли использование TestContainer для создания тестового контейнера занимать ресурсы или выполнение UT будет занимать много времени?
После тестирования контейнер MySQL был запущен в локальной среде исследований и разработок MAC. time < 20, я считаю, что в чистой среде CI/CD производительность будет выше.
Не беспокойтесь об использовании ресурсов. Контейнер занимает очень мало ресурсов, что определенно намного меньше, чем при локальной установке MySQL, и после использования он будет переработан.
Вопрос: Необходимо ли управлять контейнером, например закрывать его после использования, чтобы освободить ресурсы, чтобы избежать утечки ресурсов?
Нет, после завершения выполнения теста библиотека Testcontainers использует боковой контейнер Ryuk для автоматического удаления любых созданных ресурсов (контейнеров, томов, сетей и т. д.), и она работает надежно, даже если процесс тестирования завершается ненормально (например, отправка SIGKILL ).
Однако если вы тестируете множество промежуточных программ одновременно, вы можете их оркестровать, чтобы избежать одновременного использования контейнеров, что приведет к определенной потере ресурсов. Если у вас есть идеи или вопросы получше, оставьте сообщение в области комментариев. (Трудно быть оригинальным, пожалуйста, не перепечатывайте)
Адрес загрузки: https://github.com/fengfeihack/testcontiner_demo.