Элегантное решение UT-проблемы внешних зависимостей Testcontainer
Элегантное решение UT-проблемы внешних зависимостей Testcontainer

В нашей повседневной разработке микросервисов неизбежно использование множества сторонних сервисов. Наиболее типичным из них является MySQL. Кроме того, есть и другие. ZK,Redis,Mongo,MQ, Consul, ES и т. д. Использование множества промежуточных программ также вносит определенную сложность в процесс тестирования. Если я хочу, чтобы покрытие UT моего продукта соответствовало требованиям >90%, Так что компонентно-зависимое UT — очень хлопотная штука. В большинстве случаев мы будем использовать метод пропуска, чтобы подвергнуть все тесты зависимостей промежуточного программного обеспечения процессу интеграционного тестирования, надеясь охватить тестирование использования промежуточного программного обеспечения за счет тестирования функций продукта. Конечно, когда покрытие UT не требуется, UT, ориентированное на зависимости, также должно быть ценным, и незаменимая часть процесса исследований и разработок также оставит достаточно скрытых опасностей в нашем коде.

Почему нам нужно полагаться на UT? Разве нельзя использовать Mock (обход)?

Если нет подходящего метода UT по промежуточной цене, большинство из нас будут использовать Mock в ссылке UT. очень правильноDAOпара слоевgormиспользованияобойти, На примере MySQL мы сделаем простую демонстрацию. Полный код доступен через github.

Язык кода:go
копировать
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 используются для создания данных и запроса данных при определенных условиях соответственно.

Язык кода:go
копировать
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.

Язык кода:go
копировать
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 полей, и необходимо реализовать фильтрацию и сортировку на основе каждого поля. Код для реализации этой функции может быть следующим

Язык кода:go
копировать
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 — это не что иное, как следующие сценарии:

  • Insert mock return "err is nil"
  • Update mock return "err is nil"
  • Delete mock return "err is nil"
  • Select Mock return "err is nil and data is mock_data"

кроме выбора mock data Вот только все остальное не выглядит бессмысленным, а на самом деле бессмысленно. потому что, Например, выполнение SQL в приведенном выше случае не всегда является успешным, также существует ошибка. Например, распространенные грамматические ошибки, ошибки в написании полей, формат данных, ошибки формата времени. т. д. Тогда эти ошибки можно будет обнаружить только в процессе интеграционного тестирования. На функциональных точках с несложной логикой развертывание тестовых связей и выполнение FT позволяют выявить проблемы. Однако в развитии бизнеса всегда существуют сложные логические ссылки FT, которые представляют собой тесты «черного ящика». Как мы можем гарантировать, что каждое «если» можно протестировать? Во-вторых, даже если проблема будет обнаружена в процессе FT, потребуется рабочая сила для доработки исправления и последующего его внедрения. Протестируйте еще раз, снова потерпите неудачу, снова исправьте ........ (Даже если облачная среда поддерживает быстрое развертывание, это также нарушает менталитет разработчика)

Так как же решить проблему тестирования зависимостей?

Например, для MySQL, упомянутого выше, самый простой способ — развернуть MySQL локально, а затем подключиться для тестирования, но есть несколько проблем:

  • Варианты использования не могут быть повторно использованы. Вариант использования B, написанный A, не может быть выполнен из-за отсутствия среды;
  • В развернутой среде CI/CD также необходимо установить MySQL, который слишком зависим;
  • Если вы также полагаетесь на другие, такие как ZK, Redis, ES и т. д., каждый компонент необходимо устанавливать в локальную среду разработки, что очень затратно.
  • Если среда работает в течение длительного времени и несколько зависимых ресурсов занимают много ресурсов, ее подтягивание в реальном времени займет много времени;

Представленный сегодня артефакт Testcontainer прекрасно решает эту серию проблем.

Знакомство с инструментом Testcontainer

Testcontainers Это сторонняя библиотека зависимостей с открытым исходным кодом, используемая для поддержки модульного тестирования. Обеспечивает простой и легкий API для использования Docker Реальные сервисы в контейнерах для запуска локальной разработки и тестирования зависимостей.Зависит от промежуточного программного обеспечения。Используя Тестовые контейнеры позволяют вам писать тесты, которые зависят от тех же сервисов, что и ваша производственная среда, без использования фиктивных объектов или сервисов в памяти.

Проще говоря, это просто зависимая библиотека lib, а не служба. Во-вторых, быстро создайте необходимые вам зависимые серверы с помощью Docker-контейнеров и предоставьте их для использования. Он может поддерживать все контейнеризуемые внешние зависимости, а также поддерживает несколько распространенных языков программирования и почти все часто используемое промежуточное программное обеспечение. Благодаря полному созданию контейнеров и механизму автоматической переработки нет необходимости уделять внимание переработке контейнеров во время использования.

Студенты, которые хотят узнать больше, могут посетить официальный сайт. Официальный сайт тестконтейнеров

Преимущества использования TestContainer

  • Конфигурация инфраструктуры изоляции по требованию: Вам не нужно предварительно настраивать инфраструктуру интеграционного тестирования. Тестовый контейнер предоставит необходимые сервисы перед запуском тестов. Даже если несколько конвейеров сборки работают параллельно, вероятность заражения тестовых данных отсутствует, поскольку каждый конвейер запускает изолированный набор сервисов.
  • Получите единообразный опыт работы в локальных средах и средах CI: Вы можете напрямую получить доступ к IDE Запустите интеграционные тесты, например Модульное. Думаю то же самое. Не нужно вносить изменения и ждать CI Трубопровод завершен.
  • Надежная настройка тестирования с использованием стратегии ожидания: используется при тестировании Docker Прежде чем контейнеры их нужно запустить и полностью инициализировать. Testcontainers Библиотека предоставляет несколько готовых реализаций политики ожидания, гарантирующих полную инициализацию контейнеров (и приложений внутри них). Testcontainers Модули уже реализуют соответствующие стратегии ожидания для данной технологии, и вы всегда можете реализовать свои собственные или при необходимости создать составные стратегии.
  • Расширенные сетевые функции: Библиотека тестовых контейнеров сопоставляет порты контейнера со случайными портами, доступными на хосте, чтобы ваши тесты могли надежно подключаться к этим службам. Вы даже можете создать (Docker) сети и соединить несколько контейнеров вместе, чтобы они передавали статические Docker Сетевые псевдонимы взаимодействуют друг с другом.
  • Автоматическая очистка: После завершения выполнения теста Testcontainers Библиотека будет использовать Ryuk sidecar Контейнер автоматически удаляет любые созданные ресурсы (контейнеры, тома, сети и т. д.). При запуске необходимых контейнеров Testcontainers К созданному ресурсу (контейнеру, тому, сети и т.д.) будет прикреплен набор тегов, а также Ryuk Автоматически выполнять очистку ресурсов, сопоставляя эти теги. Он работает надежно, даже если процесс тестирования завершается ненормально (например, отправка SIGKILL).

Практика ДЕМО

На основе приведенного выше тестового кода мы создаем и используем TestContainer для модульного тестирования.

Загрузить библиотеку зависимостей Testcontainer
Язык кода:javascript
копировать
##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 

Создайте контейнер для UT

Создайте файл testhelper.go для написания кода создания зависимого контейнера.

Язык кода:javascript
копировать

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, просто импортируйте путь. Разработчикам из других команд не нужно потом уделять внимание созданию контейнеров.

Используйте TestContainer для написания UT

Язык кода:go
копировать
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 ).

Ситуация с контейнером времени выполнения TestContainer
Ситуация с контейнером времени выполнения TestContainer
Все автоматически перерабатываются после завершения.
Все автоматически перерабатываются после завершения.

Однако если вы тестируете множество промежуточных программ одновременно, вы можете их оркестровать, чтобы избежать одновременного использования контейнеров, что приведет к определенной потере ресурсов. Если у вас есть идеи или вопросы получше, оставьте сообщение в области комментариев. (Трудно быть оригинальным, пожалуйста, не перепечатывайте)

демонстрационный полный код

Адрес загрузки: https://github.com/fengfeihack/testcontiner_demo.

boy illustration
Неразрушающее увеличение изображений одним щелчком мыши, чтобы сделать их более четкими артефактами искусственного интеллекта, включая руководства по установке и использованию.
boy illustration
Копикодер: этот инструмент отлично работает с Cursor, Bolt и V0! Предоставьте более качественные подсказки для разработки интерфейса (создание навигационного веб-сайта с использованием искусственного интеллекта).
boy illustration
Новый бесплатный RooCline превосходит Cline v3.1? ! Быстрее, умнее и лучше вилка Cline! (Независимое программирование AI, порог 0)
boy illustration
Разработав более 10 проектов с помощью Cursor, я собрал 10 примеров и 60 подсказок.
boy illustration
Я потратил 72 часа на изучение курсорных агентов, и вот неоспоримые факты, которыми я должен поделиться!
boy illustration
Идеальная интеграция Cursor и DeepSeek API
boy illustration
DeepSeek V3 снижает затраты на обучение больших моделей
boy illustration
Артефакт, увеличивающий количество очков: на основе улучшения характеристик препятствия малым целям Yolov8 (SEAM, MultiSEAM).
boy illustration
DeepSeek V3 раскручивался уже три дня. Сегодня я попробовал самопровозглашенную модель «ChatGPT».
boy illustration
Open Devin — инженер-программист искусственного интеллекта с открытым исходным кодом, который меньше программирует и больше создает.
boy illustration
Эксклюзивное оригинальное улучшение YOLOv8: собственная разработка SPPF | SPPF сочетается с воспринимаемой большой сверткой ядра UniRepLK, а свертка с большим ядром + без расширения улучшает восприимчивое поле
boy illustration
Популярное и подробное объяснение DeepSeek-V3: от его появления до преимуществ и сравнения с GPT-4o.
boy illustration
9 основных словесных инструкций по доработке академических работ с помощью ChatGPT, эффективных и практичных, которые стоит собрать
boy illustration
Вызовите deepseek в vscode для реализации программирования с помощью искусственного интеллекта.
boy illustration
Познакомьтесь с принципами сверточных нейронных сетей (CNN) в одной статье (суперподробно)
boy illustration
50,3 тыс. звезд! Immich: автономное решение для резервного копирования фотографий и видео, которое экономит деньги и избавляет от беспокойства.
boy illustration
Cloud Native|Практика: установка Dashbaord для K8s, графика неплохая
boy illustration
Краткий обзор статьи — использование синтетических данных при обучении больших моделей и оптимизации производительности
boy illustration
MiniPerplx: новая поисковая система искусственного интеллекта с открытым исходным кодом, спонсируемая xAI и Vercel.
boy illustration
Конструкция сервиса Synology Drive сочетает проникновение в интрасеть и синхронизацию папок заметок Obsidian в облаке.
boy illustration
Центр конфигурации————Накос
boy illustration
Начинаем с нуля при разработке в облаке Copilot: начать разработку с минимальным использованием кода стало проще
boy illustration
[Серия Docker] Docker создает мультиплатформенные образы: практика архитектуры Arm64
boy illustration
Обновление новых возможностей coze | Я использовал coze для создания апплета помощника по исправлению домашних заданий по математике
boy illustration
Советы по развертыванию Nginx: практическое создание статических веб-сайтов на облачных серверах
boy illustration
Feiniu fnos использует Docker для развертывания личного блокнота Notepad
boy illustration
Сверточная нейронная сеть VGG реализует классификацию изображений Cifar10 — практический опыт Pytorch
boy illustration
Начало работы с EdgeonePages — новым недорогим решением для хостинга веб-сайтов
boy illustration
[Зона легкого облачного игрового сервера] Управление игровыми архивами
boy illustration
Развертывание SpringCloud-проекта на базе Docker и Docker-Compose