Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 24 из 25 ~18 мин +20 XP

Тестирование потребителей без брокера

Юнит-тест потребителя в лоб требует живого брокера — и потому становится медленным и дрожащим. Тестирование rabbitmq начинается с другой архитектуры: обработчик Delivery как функция, которая возвращает решение, транспорт с фейковым интерфейсом и интеграционный слой только для брокерных вопросов. Разберём три слоя и то, что остаётся проверять на живом брокере.

Содержание урока · 9 разделов

Юнит-тест на цикл воркера из седьмого урока в лоб не пишется: чтобы проверить одну ветку обработки, нужны живой брокер, подписка и сеть. Так тесты становятся медленными и дрожащими: контейнер не поднялся, порт занят, соседний прогон оставил сообщения в общей очереди. Выход — разрезать цикл на слои: решение о доставке принимает чистая функция, транспорт со своими Ack и Nack — тонкая обёртка, а брокер нужен только самым внешним тестам. Ключ к разделению — тип amqp.Delivery: это обычная структура с экспортными полями, её можно собрать в тесте руками и скормить обработчику, как любой другой аргумент. Никакой магии в доставке нет: Body, DeliveryTag, Headers, Redelivered — поля, которые в бою заполняет библиотека, а в тесте — вы. Ниже — как вытащить логику из цикла, подменить канал фейком и что при этом честно остаётся проверять на живом брокере.

Тестирование rabbitmq: обработчик как функция#

Начинается тестирование rabbitmq с одной перестановки: обработчик не зовёт методы канала, а возвращает решение. Причина честная: d.Ack и d.Nack требуют живого канала — в юнит-тесте его нет, а мокать целый канал ради одного кейса жалко. Три решения покрывают цикл воркера: Ack — успех, Requeue — временная неудача из восьмого урока, Park — яд в DLQ из шестнадцатого. Функция принимает Delivery и зависимости через интерфейсы: Sender для отправки письма, словарь для дедупликации дублей после requeue. Зависимости через интерфейсы — старая история из курса по Go: тест подставляет фейк, продакшен — реальный клиент SMTP, и никто из них не знает о брокере. Нюанс про JSON: поля структуры Event совпадают с ключами без тегов, Unmarshal сопоставляет имена без учёта регистра; в своём проекте вы, скорее всего, поставите теги — механика тестирования от этого не меняется. Главное приобретение: каждая ветка решения — теперь вызов функции с заранее собранными аргументами, а не сценарий с сетью.

Обработчик возвращает решение

GO
type Sender interface {
        Send(channel, orderID string) error
}

type Decision int

const (
        Ack Decision = iota
        Requeue
        Park
)

func handle(d amqp.Delivery, s Sender, seen map[string]bool) Decision {
        var ev Event
        if err := json.Unmarshal(d.Body, &ev); err != nil {
                return Park // яд: повторение не лечит сломанный формат
        }
        if seen[ev.OrderID] {
                return Ack // дубль после requeue: работа уже сделана
        }
        if err := s.Send(ev.Channel, ev.OrderID); err != nil {
                return Requeue // временная неудача: повтор из урока 16
        }
        return Ack
}

Функция чистая ровно настолько, насколько чисты зависимости: Sender подменяется, seen передаётся снаружи, Delivery собирается в тесте. Битый JSON — Park, дубль — Ack без второй отправки, ошибка Sender — Requeue: три ветки из боевого цикла, каждая проверяется вызовом без сети. Транспорт отдельно решает, как исполнить решение: это следующий блок.

Мок amqp go: фейк на своём интерфейсе#

Запрос мок amqp go в поисковике упирается в стену: amqp.Channel — конкретная структура без экспортированного интерфейса, подменить её целиком нельзя, и это осознанный дизайн библиотеки. Правильный ход — не мокать чужой тип, а определить свой узкий интерфейс из тех методов, что реально использует транспорт: Ack и Nack с точными сигнатурами amqp091-go. Конкретный amqp.Channel реализует такой интерфейс автоматически — код транспорта не меняется, а тест получает фейк со счётчиками. Правило экономности: интерфейс растёт вместе с кодом, который его использует; интерфейс на весь канал с двумя десятками методов возвращает боль мокания, от которой уходили. Такой фейк проверяет главное: что решение обработчика превратилось в правильный вызов транспорта — ack по нужному тегу, nack с верным requeue. Логику брокера фейк не проверяет: для неё есть интеграционный слой в конце урока, и путать эти уровни так же вредно, как мокать всё подряд.

Фейки, транспорт и тест решений

GO
type Completer interface {
        Ack(tag uint64, multiple bool) error
        Nack(tag uint64, multiple, requeue bool) error
}

type fakeSender struct{ err error }

func (f *fakeSender) Send(channel, orderID string) error { return f.err }

type fakeCompleter struct {
        acks, nacks int
        requeue     bool
}

func (f *fakeCompleter) Ack(tag uint64, multiple bool) error {
        f.acks++
        return nil
}

func (f *fakeCompleter) Nack(tag uint64, multiple, requeue bool) error {
        f.nacks++
        f.requeue = requeue
        return nil
}

func run(d amqp.Delivery, c Completer, s Sender, seen map[string]bool) error {
        switch handle(d, s, seen) {
        case Ack:
                return c.Ack(d.DeliveryTag, false)
        case Requeue:
                return c.Nack(d.DeliveryTag, false, true)
        case Park:
                return c.Nack(d.DeliveryTag, false, false) // в DLQ
        }
        return nil
}

func TestHandle(t *testing.T) {
        ok := amqp.Delivery{
                Body:        []byte("{"orderid":"42","channel":"email"}"),
                DeliveryTag: 1,
        }
        bad := amqp.Delivery{Body: []byte("битый формат"), DeliveryTag: 2}

        s := &fakeSender{err: errors.New("smtp лежит")}
        if got := handle(bad, s, map[string]bool{}); got != Park {
                t.Errorf("яд: хотели Park, получили %v", got)
        }
        if got := handle(ok, s, map[string]bool{}); got != Requeue {
                t.Errorf("smtp лежит: хотели Requeue, получили %v", got)
        }
}

Completer повторяет два метода *amqp.Channel — реальный канал реализует интерфейс сам, без обёрток. Фейки — структуры со счётчиками: тест утверждает не то, что было вызвано «что-то», а что вызван правильный метод с правильными флагами. Сигнатуры совпадают с библиотекой до буквы, поэтому при переходе на боевой канал транспорт не замечает подмены.

Что проверяем и где#

СлойЧто проверяетИнструменты
решенияветки handle: яд, дубль, ошибка, успехсобранный Delivery, фейковый Sender
транспортрешение превратилось в правильный вызовfakeCompleter со счётчиками
дожимворкер доедает поток и выходитchan amqp.Delivery с close
интеграцияDLX, TTL, подтверждения, обрывыживой брокер, уникальные имена
гонкигорутины не воюют за состояниеgo test -race

Подмена потока доставок: тест дожима#

Цикл воркера из семнадцатого урока читает Go-канал с Delivery — и это подарок для тестов: поток подписки подменяется обычным chan amqp.Delivery, который тест наполняет и закрывает. Закрытие потока — то же событие, что отмена контекста из двадцать первого урока: воркер доедает взятые доставки и выходит из range, не зная, что подписка была тестовой. Так дожим проверяется без сети за миллисекунды: две доставки, close, утверждения по счётчикам фейка. В тесте ценны границы: пустой поток, доставка с битым JSON после валидной, повторная доставка с Redelivered=true из восьмого урока. Если воркер запускается горутиной, разделяйте завершение через WaitGroup или закрытие канала — иначе гонка будет в тесте, а не в коде, и race detector укажет на неё честно.

Тест дожима без сети

GO
func worker(tag string, msgs <-chan amqp.Delivery, c Completer, s Sender) {
        seen := map[string]bool{}
        for d := range msgs { // закрытие канала имитирует отмену ctx
                _ = run(d, c, s, seen)
        }
}

func TestDrain(t *testing.T) {
        c := &fakeCompleter{}
        s := &fakeSender{}
        msgs := make(chan amqp.Delivery, 2)
        msgs <- amqp.Delivery{
                Body:        []byte("{"orderid":"1","channel":"push"}"),
                DeliveryTag: 1,
        }
        msgs <- amqp.Delivery{Body: []byte("битый"), DeliveryTag: 2}
        close(msgs)

        worker("t", msgs, c, s)

        if c.acks != 1 || c.nacks != 1 {
                t.Fatalf("хотели ack и nack, получили ack=%d nack=%d", c.acks, c.nacks)
        }
}

Две доставки, закрытие, один прогон воркера — и счётчики фейка показывают, что успешная доставка получила ack, а битая ушла в DLQ через nack. Тот же воркер в бою читает поток из ConsumeWithContext: разница только в источнике. Тест выполняется за микросекунды и прогоняется с -race на каждом коммите.

Интеграционные тесты с брокером#

Слой без брокера не отвечает на вопросы, ради которых очередь существует: доходят ли сообщения до DLX, срабатывает ли TTL, признает ли брокер подтверждения в правильном порядке. Это закрывают интеграционные тесты: поднимите контейнер из второго урока или testcontainers-go, пометьте файл билд-тегом go:build integration, чтобы обычный go test его не собирал. Имена очередей в тестах уникальные — имя теста плюс случайный суффикс — и удаляются в t.Cleanup: параллельные пакеты и два прогона подряд не мешают друг другу. Проверять только здесь: маршруты dead-lettering, x-message-ttl, подтверждения канала, поведение при обрыве соединения из двадцатого урока. Без Docker такие тесты пропускаются через t.Skip по проверке порта — честная деградация вместо красного билда, о которой команда знает по правилам CI.

Пирамида тестов потребителя#

  • Чистые решения: handle возвращает Decision — тесты без брокера и сети за микросекунды.
  • Транспорт: фейковый Completer проверяет, что решение превратилось в правильный Ack или Nack.
  • Дожим: подменённый chan amqp.Delivery с close вместо отмены контекста.
  • Интеграция: живой брокер с уникальными именами очередей и очисткой через t.Cleanup.
  • Гонки: go test -race на всём, где есть горутины, включая фейки.
  • Сценарий: внешний тест деплоя — сообщение проходит путь от продюсера до письма.

Разрезали мы циклы из соседних глав: седьмой урок про ConsumeWithContext дал цикл воркера, семнадцатый про worker pool — дожим с WaitGroup, восьмой про ack — решения Requeue и флаг Redelivered, шестнадцатый про dead letter — маршрут Park. Брокер для интеграционного слоя поднимает второй урок, а приёмы тестирования на Go остались в шестнадцатом уроке курса по Go. Карта курса — на странице курса.

Проверка: почему не мокать канал

Почему *amqp.Channel нельзя подменить моком через интерфейс библиотеки?

Проверка: уникальные имена очередей

Интеграционные тесты идут против одного брокера. Зачем уникальное имя очереди на каждый тест?

Какой порядок правил в decide?

Прочитайте код и выберите, что он выведет:

func decide(parseErr, redelivered bool) string {
        switch {
        case parseErr:
                return "park"
        case redelivered:
                return "requeue"
        default:
                return "ack"
        }
}

fmt.Print(decide(false, false), decide(true, false), decide(false, true))

Соберите тест решения

Допишите сборку доставки, вызов обработчика, ожидаемое решение и фейк.

s := &fake{} d := amqp.{Body: []byte(body), DeliveryTag: 1} got := (d, s, map[string]bool{}) if got != { t.Errorf("хотели Park, получили %v", got) }

Соберите тест цикла воркера

Расставьте строки: поток, доставка, закрытие, фейки, прогон, проверка счётчиков.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: тестирование потребителя

Термин

Decision-паттерн

нажмите, чтобы перевернуть
Определение

Обработчик возвращает Ack, Requeue или Park, а не зовёт методы канала: решения тестируются без брокера, транспорт остаётся тонким.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Обработчик возвращает решение, а не зовёт методы канала: d.Ack требует живого канала, которого в юнит-тесте нет.
  • *amqp.Channel — конкретная структура: мокают свой узкий интерфейс, который боевой канал реализует сам.
  • amqp.Delivery собирается в тесте руками: Body, DeliveryTag и Redelivered — обычные поля.
  • Дожим тестируется подменой потока: chan amqp.Delivery с close — то же событие, что отмена контекста.
  • Интеграционный слой — живой брокер, уникальные имена очередей, t.Cleanup и билд-тег integration.
  • go test -race обязателен: гонки ловятся и в коде, и в фейках.
  • Тесты на ошибки дешевле всего: закрытый фейк-поток закрепляет тихий выход воркера.

Домашнее задание

Разрежьте воркера из семнадцатого урока по слоям урока: handle с решениями, run с Completer, цикл с подменяемым потоком. Покройте handle таблицей тестов — битый JSON, дубль по OrderID, ошибка Sender, успех — и прогоните с -race. Затем напишите тест дожима на закрытом канале с тремя доставками и один интеграционный тест против контейнера из второго урока: сообщение доходит до DLQ после трёх ошибок, очередь удаляется в t.Cleanup, а без Docker тест пропускается через t.Skip.

Личные заметкиПусто
Хранятся только в вашем браузере (RabbitMQ · Тестирование потребителей без брокера)

Вопросы и ответы по уроку

Почему не сделать один интерфейс на весь канал?

Интерфейс на два десятка методов означает, что каждый новый метод канала требует обновления фейка, а тесты начинают проверять фейк вместо кода. Узкий интерфейс из того, что реально использует транспорт, дешевле в поддержке и точнее описывает контракт. Если бизнес-коду понадобятся другие методы — например, Publish, — добавляйте их по мере появления, не заранее.

Стоит ли гонять юнит-тесты против эмулятора брокера?

Эмуляторов AMQP, повторяющих поведение брокера в деталях — dead-lettering, TTL, подтверждения, — на сегодня нет, а частичные имитируют ровно то, что ломается в продакшене. Честнее разделение: юнит — на решениях и фейках, интеграция — на живом брокере в контейнере. Посередине остаются трата времени и ложная уверенность, которые хуже отсутствия теста.

Как тестировать publish-сторону?

Той же техникой: узкий интерфейс Publisher с сигнатурой PublishWithContext, фейк со слайсом сохранённых Publishing, утверждения по mandatory, DeliveryMode и заголовкам. Интеграционный тест с NotifyReturn и подтверждениями проверит то, что фейк не может: маршрутизацию и вердикты брокера. Симметрия слоёв читается на ревью как единый стиль тестирования очереди.

Термины урока

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

Следующая ступень учебного пути

Excelize25 уроков~10 чпрактика25 уроков уже доступны

Excel в Go: чтение и генерация xlsx на excelize

Курс excelize на Go: создание xlsx, чтение, стили, формулы, диаграммы и StreamWriter для больших файлов. 25 уроков с примерами кода и проектом.

  • Создавать и читать xlsx-файлы средствами excelize
  • Работать с листами, ячейками, строками и столбцами
  • Оформлять таблицы: стили, шрифты, границы, форматы чисел и дат
  • Вставлять формулы, диаграммы и изображения