
Из этой статьи вы узнаете, зачем нужны моки для модульного тестирования операторов 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.
Узнать подробнее: https://slurm.club/3y2DzBa
