Из этой статьи вы узнаете, зачем нужны моки для модульного тестирования операторов Kubernetes и как их писать. Эти концепции применимы к операторам на разных языках и фреймворках. Здесь мы будем использовать Golang, controller-runtime и библиотеку testify. Предполагается, что вы хорошо разбираетесь в Kubernetes, операторах и тестировании программного обеспечения.

В этой статье мы будем работать с example-operator, который можно взять здесь.

Модульные тесты и моки — что это и зачем

Зачем вообще делать модульные тесты (unit test)? Судя по названию, речь о тестировании отдельных модулей программного обеспечения. Мы хотим, чтобы каждый компонент работал, как задумано, независимо от других компонентов в коде и за его пределами. Это хороший способ искать проблемы в программном обеспечении. Отдельные части программы будут слаженно работать друг с другом, только если сами по себе работают правильно.

Если мы говорим о контроллерах Kubernetes, этими модулями можно считать отдельные функции (или методы), которые взаимодействуют с Kubernetes API (через клиент) и применяются к объектам и ресурсам Kubernetes. Функции должны взаимодействовать с Kubernetes API, но модульные тесты этого не предусматривают. Во-первых, мы стремимся изолировать тестируемый код, а во-вторых, будет дороговато создавать сторонние ресурсы.

И тут на помощь приходят моки (mock), имитирующие сервисы, от которых зависит компонент кода. В нашем случае это клиент Kubernetes. Моки помогают протестировать, как контроллер взаимодействует с Kubernetes API, например убедиться, что некоторые операции выполняются. При этом нам не нужен сам кластер Kubernetes.

Рис. 1. Тестирование с помощью моков
Рис. 1. Тестирование с помощью моков

Разбор кода

Цикл согласования контроллера запускается при каждом действии с объектом 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

Комментарии (0)