Настольные тесты - это хорошо, но слышали ли вы о золотом файле?

Если вы новичок в Голанге, возможно, вы не слышали о золотом файле. Я узнал об этом после двух лет программирования на Go, когда наткнулся на доклад Митчелла Хашимото о Advanced Testing in Go. По этому поводу не было официальной документации, не многие блоги упоминают его, и никто не знает, почему он называется золотым файлом. Все, что мы знаем, это то, что он есть в стандартном пакете: gofmt / testdata.

В этом посте объясняется, как золотой файл является полезной альтернативой традиционным табличным тестам в Golang для конкретных сценариев.

Что такое золотой файл?

Концептуально в этом нет ничего нового. Если вы работаете с Javascript и раньше использовали библиотеку тестирования Jest, это в основном то, что Jest называет тестированием моментальных снимков. Он сохраняет результат теста в файл и использует его как ожидаемый результат.

Это тестовый прием, зачем кому-то это делать?

Лучше всего это можно объяснить на примере реального случая использования, который я испытал в команде по борьбе с мошенничеством в GOJEK. Представьте себе упрощенную схему микросервиса, которую мы строим ниже:

Микрослужба действует как прокси-служба, которая принимает HTTP-запросы, выполняет некоторую бизнес-логику и инициирует вызовы API к другому микросервису. В частности, он предназначен для отправки коммуникационных сообщений в соответствующий API на основе полученного запроса, то есть push-уведомления, SMS или подсказки в приложении. Чтобы убедиться, что все работает должным образом, мы написали тест на основе таблиц:

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

Здесь я хочу выделить три наблюдения:

  1. Прежде всего, настольный тест великолепен. Время, потраченное на реализацию шаблона и цикла, который выполняет итерацию по таблице, стоит накладных расходов на добавление различных тестовых сценариев. Я очень рекомендую использовать этот паттерн.
  2. По мере того как наши тестовые примеры и количество утверждений росли, наша таблица становилась слишком загроможденной. Разработчикам стало труднее понимать и изменять тестовые примеры.
  3. Одно из утверждений, которое мы хотим сделать, - убедиться, что мы отправляем правильный HTTP-заголовок и тело другому микросервису. Это та часть, которая в первую очередь раздувает наш стол до неприемлемой для человека ситуации 🤖

Вот наша золотая возможность

К счастью, у нас есть золотая пилка. Этот метод наиболее полезен, когда кто-то хочет подтвердить огромный кусок байтов, такой как строка базы 64 изображения, файл JSON или (в нашем случае) тело HTTP. Вместо того, чтобы записывать ожидаемое тело JSON в тестовый файл, мы можем сохранить его вtestdata/TestAPI_Comms_Success.golden. Затем мы можем вручную создать золотой файл, хотя лучше использовать флаг update. Это сделано для того, чтобы тест знал, собираемся ли мы обновить золотой файл или подтвердить результат теста против золотого файла. Давайте изменим наше утверждение:

Если вы еще не поняли, дополнительный код в функции TestAPI_Comms_Success по существу сбрасывает logs таблицу в TestAPI_Comms_Success/<test-case-name>.golden в усовершенствованном формате JSON. Итак, теперь есть два режима для запуска теста:

  1. Обновить
go test ./... -update

Когда передается флаг обновления, все, что содержится в таблице журналов, будет сохранено в золотой файл. Например:

[{
  "id": 1,
  "http_method": 
  "entity_id": 1, 
  "entity_type": "driver", 
  "action": "comm1", 
  "api_name": "service-A", 
  "http_method": "POST",
  "http_body": {
    "type": "comm-service", 
    "action": "send-notification", 
    "priority": "medium", 
    "when": "now"
  },
  "http_response_status_code": 200
}]

2. Утвердить

go test ./...

Без флага обновления код выполнит запрос, извлекающий журналы из базы данных. Затем он сравнивает его с тем, что было записано в золотой файл ранее.

В чем подвох?

При тестировании золотого файла наиболее важным шагом является ручной просмотр золотого файла, чтобы убедиться, что сохраненный результат соответствует нашим ожиданиям. Вот почему мы распечатали JSON в улучшенном формате, чтобы эта задача упростилась.

Более сложные сценарии

Если описанной выше настройки достаточно для тестирования золотого файла, это прекрасно. К сожалению для нас, необходимо решить три проблемы:

1. Асинхронная архитектура

Наш микросервис использует асинхронную архитектуру, в которой есть очереди сообщений (RabbitMQ) и рабочие, которые принимают задания. В этом случае, когда мы хотим сгенерировать золотой файл, мы точно не знаем, когда читать журналы из базы данных. Журнал может быть записан работником через 50 мс, через 100 мс или через 300 мсек.

Решение состоит в том, чтобы добавить time.Sleep() с количеством времени, когда вы уверены, что работник закончит работу. В качестве альтернативы вы можете использовать В конце концов G omega. Он может опрашивать ваше асинхронное утверждение с желаемой частотой и тайм-аутом.

2. Поле времени / UUID

Наша таблица журналов содержит такие поля, как log_id, created_at, updated_at,, которые всегда меняются при создании золотого файла. Один из способов решить эту проблему - исключить эти поля при запросе в базе данных золотого файла. Если у вас есть много таблиц для утверждения в золотой файл (как у нас), monkeypatch - хорошее решение.

// Patch time.Now() to static time so that we can compare golden files correctly
timezone, err := time.LoadLocation("Asia/Jakarta")
timeNowPatch := monkey.Patch(time.Now, func() time.Time {
  return time.Date(2012, time.December, 5, 15, 0, 0, 0, timezone)
})

Единственная загвоздка здесь в том, что когда вы записываете журнал в базу данных, вы должны передавать time.Now() в качестве аргумента в поле created_at вместо использования вашей функции Postgres now().

Теперь для UUID мы используем https://github.com/google/uuid. Что мы можем сделать, так это загрузить статический считыватель случайных чисел, чтобы сгенерированные UUID всегда были одинаковыми:

reader := bytes.NewReader([]byte("1111111111111111"))
uuid.SetRand(reader)
uuid.SetClockSequence(1)

3. Лучшее отличие золотого файла

Написание вывода, такого как There is a mismatch in TestAPI_Comms_Success.golden, не является полезной информацией, если ваш золотой файл состоит из 200 строк. Нужно смотреть поле за полем и сравнивать, какое из них отличается. Чтобы упростить нашу жизнь, мы используем gomega.MatchJSON, который указывает, какой ключ JSON имеет несоответствие.

Подходит ли мне золотая пилка?

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

GOJEK управляет одним из крупнейших кластеров го в Азии. С момента запуска приложения в 2015 году мы увеличили масштаб более чем в 6600 раз, а в 2018 году расширились до Вьетнама, Сингапура и Таиланда. Мы продолжаем расти и постоянно учимся. Перейдите на сайт gojek.jobs и станьте частью нашего пути по новому пониманию транспорта, платежей и логистики в Юго-Восточной Азии 🙌