Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Из этой статьи вы узнаете, зачем нужны моки для модульного тестирования операторов Kubernetes и как их писать. Эти концепции применимы к операторам на разных языках и фреймворках. Здесь мы будем использовать Golang, controller-runtime и библиотеку testify. Предполагается, что вы хорошо разбираетесь в Kubernetes, операторах и тестировании программного обеспечения.
В этой статье мы будем работать с example-operator, который можно взять здесь.
Модульные тесты и моки — что это и зачем
Зачем вообще делать модульные тесты (unit test)? Судя по названию, речь о тестировании отдельных модулей программного обеспечения. Мы хотим, чтобы каждый компонент работал, как задумано, независимо от других компонентов в коде и за его пределами. Это хороший способ искать проблемы в программном обеспечении. Отдельные части программы будут слаженно работать друг с другом, только если сами по себе работают правильно.
Если мы говорим о контроллерах Kubernetes, этими модулями можно считать отдельные функции (или методы), которые взаимодействуют с Kubernetes API (через клиент) и применяются к объектам и ресурсам Kubernetes. Функции должны взаимодействовать с Kubernetes API, но модульные тесты этого не предусматривают. Во-первых, мы стремимся изолировать тестируемый код, а во-вторых, будет дороговато создавать сторонние ресурсы.
И тут на помощь приходят моки (mock), имитирующие сервисы, от которых зависит компонент кода. В нашем случае это клиент Kubernetes. Моки помогают протестировать, как контроллер взаимодействует с Kubernetes API, например убедиться, что некоторые операции выполняются. При этом нам не нужен сам кластер Kubernetes.
Разбор кода
Цикл согласования контроллера запускается при каждом действии с объектом Deployment. Контроллер согласует Deployments, внедряя дополнительный контейнер в шаблон пода в зависимости от наличия пары «метка-значение» container/inject=true.
Этот контроллер ничего полезного не делает, он написан только для этой статьи.
Метод Reconcile, который можно найти в controller/controller.go, выступает как точка входа для всех действий, связанных с согласованием.
// ... controller/controller.go
type MyReconciler struct {
client.Client
Scheme *runtime.Scheme
}
func (r *MyReconciler) Reconcile(
ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// STEP 1: get the deployment object
deployment := &appsv1.Deployment{}
err := r.Get(ctx, req.NamespacedName, deployment)
if err != nil {
return ctrl.Result{}, err
}
// STEP 2: reconcile
if err := r.handleDeploymentReconciliation(ctx, deployment); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
Основная логика согласования инкапсулирована в метод handleDeploymentReconciliation, для которого мы и пишем модульный тест. Зависимость, которую мы будем здесь имитировать, — клиент Kubernetes, предоставляемый controller-runtime.
Пишем моки для клиента Kubernetes
Модуль stretchr/testify предоставляет пакет mock, с помощью которого можно легко писать кастомные моки для модульных тестов. Давайте напишем мок, который будет имитировать клиент Kubernetes
Весь код, связанный с моком: utils/tesutil.go.
Для начала создадим мок для Client, внедрив mock.Mock в его структуру.
// ... utils/testutil.go
type Client struct {
mock.Mock
....
}
Теперь укажем нужные методы для мока Client, который будет вызываться методом handleDeploymentReconciliation.
// ... utils/testutil.go
func (c *Client) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
args := c.Called(ctx, obj, opts)
return args.Error(0)
}
Метод Update не делает здесь ничего полезного. Он просто сообщает моку, что его вызвали, и возвращается без ошибок. В реальном клиенте этот метод что-то делал бы, но мы его просто имитируем, так что используем заглушку.
Для краткости возьмём только метод Update. Вообще-то мок Client должен реализовать все методы, определённые в интерфейсе Client. Код мока можно создать автоматически с помощью mockery.
Пишем модульные тесты
Мы создали мок, а теперь используем его при написании модульного теста. Модульные тесты: controller/controller_test.go .
// ... controller/controller_test.go
func TestHandleDeploymentReconciler(t *testing.T) {
client := utils.NewClient()
// setup expectations
client.On("Update",
mock.IsType(context.Background()),
mock.IsType(&appsv1.Deployment{}),
mock.Anything,
).Return(nil)
ctx := context.Background()
reconciler := &MyReconciler{
Client: client,
Scheme: newTestScheme(),
}
err := reconciler.handleDeploymentReconciliation(ctx, newTestDeployment())
require.NoError(t, err)
client.AssertExpectations(t)
}
Мы используем механизм из набора testify, чтобы убедиться, что ожидаемый вызов функции Update произошёл с правильными типами аргументов и возвращаемых значений.
client.On задаёт ожидания о том, какой метод клиента (в нашем случае это Update) нужно вызвать и с какими типами аргументов и возвращаемых значений.
mock.IsType проверяет, что ожидаемый метод (у нас это Update), настроенный с помощью client.On, вызывается с правильными типами аргументов.
client.AssertExpectations проверяет соответствие ожиданиям. Тест сообщает, если вызываются неожиданные методы, а ожидаемые методы не вызываются или вызываются с неожиданными типами аргументов.
Наконец, require.NoError проверяет, что handleDeploymentReconciliation возвращается без ошибок.
Для запуска теста достаточно выполнить в терминале команду:
$ go test -timeout 30s -run ^TestHandleDeploymentReconciler$ \ ./controller
ok github.com/mayankshah1607/example-operator/controller 0.941s
Всё просто, правда же?
Заключение
Можно много говорить о модульном тестировании и операторах Kubernetes. В этой статье мы рассмотрели, как легко и быстро написать модульный тест для операторов Kubernetes с помощью моков. Моки — это отличный способ имитировать внешние API в тестах. Мы узнали, как использовать набор инструментов testify, чтобы писать моки для имитации клиента Kubernetes, а потом использовать их в модульных тестах. В этой статье мы писали операторы с помощью Golang и controller-runtime, но все эти принципы можно применять и с другими фреймворками.
Еще больше Kubernetes
Еще больше знаний и практик по K8s вы cможете найти на нашем продвинутом курсе Kubernetes:Мега. Там мы подробно разбираем такие темы, как Open Policy Agent, Network Policy, безопасность и высокодоступные приложения, ротация сертификатов, аутентификация пользователей в кластере, хранение секретов, Horisontal Pod Autoscaler, создание собственного оператор K8s, в общем, залезаем под капот Kubernetes.
Обучение в потоке стартует 11 ноября, а видеокурс доступен уже сейчас.
Узнать подробнее: https://slurm.club/3y2DzBa