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

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

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

Урок 16 из 25 ~18 мин +50 XP

Тестирование endpoints

Два этажа обёрток готовы — настало время проверить, что они действительно работают. Тестирование go-kit устроено проще, чем кажется: endpoint — это функция, middleware — функция над функцией, декоратор — реализация интерфейса, и всё это тестируется прямыми вызовами без серверов, портов и сна. Ты напишешь endpoint тесты как table-driven таблицу к функции, подставишь fake-next обёрткам и fake-сервис декораторам из уроков 14-15, проверишь кодеки через httptest.NewRecorder без сети, а затем поднимешь интеграционный тест транспорта на httptest.NewServer — и увидишь, что половина вопросов «а как это протестировать» уже решена самой архитектурой. Мок-транспорт в курсе — это рука-стаб на двадцать строк: тестирование go kit приучает мокать зависимости, а не библиотеку, и держать пирамиду: сервис, endpoint, обёртки, транспорт — от быстрых тестов к медленным.

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

Пирамида тестов go-kit: что и чем проверяем#

У go-сервиса четыре тестируемых этажа, и скорость теста обратно пропорциональна его ценности в рантайме: чем выше этаж, тем реже и дороже его проверяют. В основании — сервис: table-driven тесты StringService из урока 03, микросекунды на прогон, десятки кейсов на метод. Второй этаж — endpoint'ы: те же table-driven, но вызывается makeUppercaseEndpoint, и проверяется шов адаптера: type assertion, маппинг ошибок в Err-поле, Failer. Третий — обёртки обоих этажей из уроков 14 и 15: endpoint-middleware проверяются с fake-next, декораторы — с fake-сервисом; изоляция полная, никакого транспорта. Четвёртый — транспорт и кодеки: kithttp.NewServer гоняется на httptest.NewRecorder без сети, интеграционный сценарий — на httptest.NewServer с реальным HTTP-циклом. Тестирование go kit дорожает по этажам: сервис — миллисекунды, endpoint — миллисекунды, middleware — единицы миллисекунд, транспорт — десятки. Держи пропорцию пирамиды: много быстрых тестов внизу, горстка интеграционных наверху; end-to-end прогон с curl оставь деплою. Ключ к этажу — простая мысль: в go-kit почти всё — функции, а функции тестируются вызовом.

Тест endpoint: это просто функция#

Начни с главного приёма курса. Endpoint — сигнатура func(ctx, request) (response, error) — зовётся в тесте напрямую: ep := makeUppercaseEndpoint(svc); resp, err := ep(context.Background(), UppercaseRequest{S: "go"}). Никакого декодера, никакого HTTP: таблица кейсов с входной строкой, ожидаемым V и ожидаемой ошибкой — почти копия table-driven теста сервиса из урока 03, только проверяется ещё и адаптер. Что здесь проверяется, а что нет. Проверяется: assertion request-структуры, вызов метода сервиса, упаковка ответа, перенос ErrEmpty в Err-поле через Failer — весь шов из урока 02. Не проверяется: JSON, статусы, заголовки — это транспорт, его тест ниже. Вторая причина endpoint тестов — обёртки: makeUppercaseEndpoint(svc) принимает декорированный сервис, поэтому один и тот же тест можно прогнать на чистом сервисе и на матрёшке с кэшем из урока 15 — и это уже тест интеграции этажей, всё ещё без сети. Ошибки в тестах проверяются через errors.Is на sentinel: строковое сравнение err.Error() ломается при первой же обёртке fmt.Errorf с %w из урока 14.

Table-driven тест endpoint

GO
func TestUppercaseEndpoint(t *testing.T) {
        ep := makeUppercaseEndpoint(stringService{})

        cases := []struct {
                name    string
                in      string
                want    string
                wantErr error
        }{
                {"обычная", "go", "GO", nil},
                {"пустая", "", "", ErrEmpty},
        }
        for _, c := range cases {
                t.Run(c.name, func(t *testing.T) {
                        resp, err := ep(context.Background(), UppercaseRequest{S: c.in})
                        if c.wantErr != nil {
                                if !errors.Is(err, c.wantErr) {
                                        t.Fatalf("err = %v, хочу %v", err, c.wantErr)
                                }
                                return
                        }
                        if err != nil {
                                t.Fatalf("неожиданная ошибка: %v", err)
                        }
                        if resp.(UppercaseResponse).V != c.want {
                                t.Fatalf("V = %v, хочу %v", resp.(UppercaseResponse).V, c.want)
                        }
                })
        }
}

Три проверки — на ошибку, на отсутствие ошибки и на значение: пропуск любой из веток превращает тест в решето. errors.Is на ErrEmpty — канон: sentinel остаётся различимым сквозь Failer и любые будущие обёртки ошибок. Запрос строится литералом структуры — тест не знает про JSON и умеет только сигнатуру endpoint'а.

Тест middleware: fake-next на двадцать строк#

Endpoint-middleware тестируется подстановкой next: обёртка — функция над функцией, значит в тест next ставится стаб, поведение которого ты контролируешь полностью. Два шаблона. Первый — сигнальный next: стаб-функция ставит флаг «меня вызвали» или паникует по требованию — им проверяется, что лимитер не пропустил вызов, что timeout отменил контекст, что recovery превратил панику в ошибку. Второй — записывающий next: стаб фиксирует контекст и аргументы, им проверяются обёртки-трансформеры вроде errwrap из урока 14. Отдельная прелесть — логгер-стаб: реализация log.Logger из урока 06 — один метод Log(keyvals ...interface{}) error; двадцать строк собирают логгер, который копит пары в срез под мьютексом, и middleware логирования можно проверять дословно. Мокается только next и зависимости, потому что именно они интерфейсы; сами endpoint middleware — конкретные функции, и тестируются как функции. Так и работает тестирование go kit: мокать то, что твоя обёртка вызывает, а не то, что она есть.

Тест recovery: next паникует

GO
func TestRecoveryMiddleware(t *testing.T) {
        logger := newTestLogger() // стаб из хелперов ниже
        ep := recoveryMiddleware(logger)(func(ctx context.Context, req interface{}) (interface{}, error) {
                panic("бум")
        })

        resp, err := ep(context.Background(), UppercaseRequest{S: "go"})
        if err == nil {
                t.Fatal("хотели ошибку, получили nil")
        }
        if resp != nil {
                t.Fatalf("ответ должен быть nil, получен %v", resp)
        }
        if !strings.Contains(logger.text(), "panic: бум") {
                t.Fatalf("в логе нет паники: %s", logger.text())
        }
}

// next без паники — проверка прозрачности:
func TestRecoveryHappyPath(t *testing.T) {
        ep := recoveryMiddleware(newTestLogger())(okEndpoint) // возвращает ("OK", nil)
        resp, err := ep(context.Background(), UppercaseRequest{S: "go"})
        if err != nil || resp.(UppercaseResponse).V != "OK" {
                t.Fatalf("здоровый путь испорчен: %v %v", resp, err)
        }
}

Первый тест — негативный сценарий: паника должна стать err, ответ — nil, лог — содержать значение паники. Второй — обязательный парный: обёртка не должна ломать здоровый путь, и два теста всегда пишутся рядом. okEndpoint и newTestLogger — хелперы пакета тестов; заметь, что стаб-next — обычный литерал функции, типы совпадают с endpoint.Endpoint без объявлений.

Логгер-стаб: log.Logger за 15 строк

GO
type testLogger struct {
        mu  sync.Mutex
        kvs [][]interface{}
}

func (l *testLogger) Log(keyvals ...interface{}) error {
        l.mu.Lock()
        defer l.mu.Unlock()
        l.kvs = append(l.kvs, keyvals)
        return nil
}

func (l *testLogger) text() string {
        l.mu.Lock()
        defer l.mu.Unlock()
        parts := make([]string, 0, len(l.kvs)*2)
        for _, kv := range l.kvs {
                parts = append(parts, fmt.Sprint(kv...))
        }
        return strings.Join(parts, " ")
}

Интерфейс kitlog — один метод: интерфейсная сегрегация в чистом виде, поэтому подстановка такая дешёвая. Мьютекс обязателен: middleware логирует из горутин параллельных запросов, и тест, читающий лог во время записи, будет гнать data race — не забудь флаг -race в CI. Тот же стаб годится для endpoint- и service-логгеров: у них одинаковый контракт log.Logger.

Как поймать пролетевший вызов

Как убедиться, что NewErroringLimiter при пустом бакете не вызвал endpoint вовсе?

Тест декоратора: fake-сервис вместо реализации#

Service-middleware из урока 15 тестируется зеркально: вместо fake-next — fake-сервис, реализующий StringService. Стаб-реализация считает вызовы метода (атомарный счётчик или канал-лог), возвращает заранее заготовленные ответы и умеет ломаться по сценарию — вернуть ErrEmpty, зависнуть на канале. Классический кейс кэша: fake-сервис, поверх которого cachingMiddleware; первый вызов инкрементирует счётчик base, второй — нет, при этом оба вернули одно значение, а sentinel-ошибка не закэшировалась. Это тест целого контракта из урока 15 — три утверждения из одной таблицы. Заметка об интеграции этажей: матрёшку для теста собирай той же applyMiddleware, что и prod-код — если тест соберёт декораторы руками, а main через срез, расхождение однажды проскочит мимо тестов. И ещё: fake-сервис — это не «мок-библиотека»: для интерфейса из двух методов ручной стаб дешевле и нагляднее кодогенерации; gomock и компания уместны, когда интерфейсов десятки и обновлять стабы руками дороже, чем держать тулчейн.

Кэш на fake-сервисе: три утверждения

GO
type fakeService struct {
        calls int32
}

func (f *fakeService) Uppercase(ctx context.Context, s string) (string, error) {
        atomic.AddInt32(&f.calls, 1)
        return strings.ToUpper(s), nil
}
func (f *fakeService) Count(ctx context.Context, s string) int { return len(s) }

func TestCachingMiddleware(t *testing.T) {
        base := &fakeService{}
        svc := NewCachingMiddleware(map[string]string{})(base)

        v1, err1 := svc.Uppercase(context.Background(), "go")
        v2, err2 := svc.Uppercase(context.Background(), "go")

        if v1 != "GO" || v2 != "GO" || err1 != nil || err2 != nil {
                t.Fatalf("значения испорчены: %q %q %v %v", v1, v2, err1, err2)
        }
        if n := atomic.LoadInt32(&base.calls); n != 1 {
                t.Fatalf("base вызван %d раз, хочу 1", n)
        }
}

Атомарный счётчик вместо флага: будущие параллельные тесты кэша не сломают стаб. Средняя проверка — самая важная: именно «base вызван один раз» отличает кэш от простого проброса. Тест живёт в микросекундах: ни сети, ни портов, ни сна — обычные вызовы функций по интерфейсу.

Тест кодека: httptest.NewRecorder без сети#

Транспортный этаж проверяется двумя уровнями, и первый — без сети вообще. kithttp.NewServer возвращает http.Handler, а httptest.NewRecorder — фейковый ResponseWriter, который собирает статус и тело в память. Собери сервер, сформируй httptest.NewRequest, вызови server.ServeHTTP(w, r) — и весь HTTP-цикл прошёл, не открыв ни порта. Что здесь проверяется: декодер (сломанный JSON должен дать честный 400 из урока 05), энкодер (Content-Type application/json), error encoder (ErrEmpty → 400 с телом {"error":"empty string"}, кривая ошибка → 500), Failer-контракт. Мутации канона: этот тест — единственное место, где транспортные типы легальны в тесте; всё остальное про байты живёт в кодеках. Скорость — микросекунды на кейс, поэтому транспортная таблица может быть такой же жирной, как сервисная. Второй уровень — интеграция: httptest.NewServer поднимает настоящий listener на loopback; его цена — десятки миллисекунд, его ценность — проверка полного цикла: json.Marshal, заголовки, конверт ошибки, поведение клиента. Один-два таких теста на сервис достаточно.

Recorder: статус и тело в памяти

GO
func TestUppercaseHTTP(t *testing.T) {
        ep := makeUppercaseEndpoint(stringService{})
        server := kithttp.NewServer(
                ep,
                decodeUppercaseRequest, // из урока 04
                encodeResponse,         // с Failer и encodeError из урока 05
        )

        body := strings.NewReader("{"s":"go"}")
        req := httptest.NewRequest(http.MethodPost, "/uppercase", body)
        req.Header.Set("Content-Type", "application/json")
        w := httptest.NewRecorder()

        server.ServeHTTP(w, req)

        if w.Code != http.StatusOK {
                t.Fatalf("статус %d, хочу 200", w.Code)
        }
        if got := w.Body.String(); !strings.Contains(got, "GO") {
                t.Fatalf("тело %q не содержит GO", got)
        }
}

Три действия — собрать handler, сконструировать запрос, вызвать ServeHTTP — и никаких слушателей. Рекордер позволяет проверить всё, что реально пишет энкодер: статус, тело, заголовки через w.Header().Get. Table-driven продолжается и здесь: вторая строка таблицы — пустой s, ожидание 400 и substring empty string; третья — кривой JSON, ожидание 400 из ветки сломанного декодера.

Что упадёт в Recorder

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

req := httptest.NewRequest(http.MethodPost, "/uppercase",
        strings.NewReader("{"s":""}"))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
// encodeError из урока 05: errors.Is(err, ErrEmpty) -> 400

fmt.Println(w.Code, w.Body.String())

Интеграционный тест: httptest.NewServer#

Второй уровень транспортных тестов поднимает настоящий HTTP-сервер: httptest.NewServer(server) слушает loopback и отдаёт базовый URL; тест идёт обычным http.Client — как реальный потребитель. Ценность уровня: он проверяет стыки, которые Recorder не видит — сериализацию json.Marshal в энкодере, заголовки Content-Type обеих сторон, обработку редиректов и таймаутов клиентом, а главное — форму конверта ошибки глазами клиента: тест декодирует ответ в структуру и сверяет поле error, а не substring тела. Дедлайн теста — context.WithTimeout на запрос: зависший сервер не повесит CI, а упадёт с понятным сообщением за две секунды. Заметь честную деталь: этот тест ходит по сети — localhost, но сеть; он медленнее и чуть более хрупкий, поэтому его в одном экземпляре на критичный метод, а не на каждый кейс таблицы. Мок-транспорт, о котором спрашивают чаще всего, — это и есть httptest.NewServer: go-kit не требует мокать kithttp — сервер готов, а мокаешь ты свой сервис под ним.

Интеграция: клиент против httptest.NewServer

GO
func TestUppercaseIntegration(t *testing.T) {
        ep := makeUppercaseEndpoint(stringService{})
        handler := kithttp.NewServer(ep, decodeUppercaseRequest, encodeResponse)
        ts := httptest.NewServer(handler)
        defer ts.Close()

        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()

        req, _ := http.NewRequestWithContext(ctx, http.MethodPost,
                ts.URL+"/uppercase", strings.NewReader("{"s":"go"}"))
        req.Header.Set("Content-Type", "application/json")

        resp, err := http.DefaultClient.Do(req)
        if err != nil {
                t.Fatalf("запрос не прошёл: %v", err)
        }
        defer resp.Body.Close()

        if resp.StatusCode != http.StatusOK {
                t.Fatalf("статус %d", resp.StatusCode)
        }
        var got UppercaseResponse
        if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
                t.Fatalf("ответ не декодируется: %v", err)
        }
        if got.V != "GO" {
                t.Fatalf("V = %q", got.V)
        }
}

Декодирование в request/response-структуру — проверка контракта глазами клиента: то же поле V, тот же json-тег из урока 02. defer ts.Close() обязателен — иначе тест слушатель живёт до конца процесса и порт пачкается. Клиент с контекстом и таймаутом — привычка из урока 19: зависший тест хуже упавшего, потому что подвешивает весь прогон.

Матрица инструментов

Тебе нужно проверить, что лимитный отказ доезжает до клиента как 429 с Retry-After. Какой уровень теста выберешь?

Матрица: этаж → тест → инструмент#

ЭтажЧто проверяемИнструментСкорость
Сервис (урок 03)логика, sentinel-ошибкиtable-driven, прямой вызовмикросекунды
Endpoint (урок 02)адаптер, Failer, Err-полеtable-driven, вызов ep(ctx, req)микросекунды
Endpoint-middleware (14)перехват, порядок, ошибкиfake-next, логгер-стабмиллисекунды
Service-middleware (15)кэш, ретраи, счётчик вызововfake-сервис с атомарным счётчикоммиллисекунды
Кодеки (04-05)статусы, тело, Content-Typehttptest.NewRecorder без сетимиллисекунды
Транспортполный HTTP-цикл, клиентhttptest.NewServer + http.Clientдесятки мс

Чего не тестировать: библиотека и каркас#

Обратная сторона пирамиды — куда тесты не нужны. Не тестируй go-kit: kithttp.NewServer гарантированно вызывает endpoint, это покрыто библиотечными тестами; твой Recorder-тест уже проверяет интеграцию с твоими кодеками — дублировать внутренности пакета бессмысленно. Не тестируй обёртки других людей в их же терминах: gobreaker проверял свои состояния, твоя зона ответственности — контракт ErrOpenState → 503 в твоём encodeError. Не пиши тесты на конфигурацию в main: сборку матрёшек проверяют типы компилятора и один интеграционный тест. И не гоняйся за процентом покрытия в тестах каркаса — гони гонку и детерминизм: go test -race -count=1 в CI обязателен для всего, что касается middleware из уроков 14-15, потому что замыкания исполняются параллельными горутинами, а мьютексы и атомарные счётчики проверяются только рантаймом с гонкой. Правило выбора цели теста простое: тестируй то, что написал ты, через контракт, который объявил ты, — библиотека остаётся чёрным ящиком, твой код — белым. Финальная строка CI-конфига: go-kit тесты гоняются с -race -count=1 — привычка, которая окупается с первого инцидента.

Чья зона ответственности

Команда спорит: писать ли тест на gobreaker, что после MaxRequests в half-open цепь пропускает новые попытки?

Дедлайны и сон: тесты без flaky#

Тесты, проверяющие время, — главный источник flakiness, и у go-kit-стека есть аккуратный выход. Не пиши time.Sleep в тестах: Sleep(100ms) на быстрой машине стреляет в 99% прогонов, а на CI в час пик — в 95%, и красные тесты перестают что-либо значить. Вместо этого сжимай время: timeout-обёртка из урока 14 тестируется с дедлайном в пять миллисекунд против fake-next, который блокируется на канале без записи; TestRecovery паникует мгновенно; retry-декоратор из урока 15 конфигурируется паузой в наносекунды — зависимость паузы приходит конструктором, как и всякая другая. Если тест должен дождаться события — дождись его блокирующим чтением канала, а не опросом со сном: стаб может отдавать chan struct{}, тест читает с context.WithTimeout. Дедлайн теста — всегда: t.Deadline или контекст на запрос; упавший тест лучше зависшего. И помни про -race: обёртки, кэши и логгер-стабы — многогорутинный код, гонка в тесте — баг в проде, который успел проголосовать.

Логгер-стаб и порядок записей

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

logger := newTestLogger()
svc := NewLoggingMiddleware(logger)(stringService{})
svc.Uppercase(context.Background(), "go")

// Из predict урока 03: внутренний пишет первым.
// Теперь — endpoint-этаж:
ep := loggingMiddleware(logger)(makeUppercaseEndpoint(svc))
ep(context.Background(), UppercaseRequest{S: "go"})

Собери Recorder-тест

Восстанови транспортный тест без сети: три пропуска.

func TestUppercaseHTTP(t *testing.T) { server := kithttp.NewServer( makeUppercaseEndpoint(stringService{}), decodeUppercaseRequest, encodeResponse, ) req := httptest.(http.MethodPost, "/uppercase", strings.NewReader("{"s":"go"}")) w := httptest.() server.(w, req) if w.Code != http.StatusOK { t.Fatalf("статус %d, хочу 200", w.Code) } }

Путь теста от базы пирамиды к вершине

Расставь шаги прогонки: от самого быстрого теста к самому дорогому.

Строки

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

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

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

Термин

Endpoint-тест

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

Прямой вызов ep(ctx, request) в table-driven таблице: проверяется шов адаптера, Failer и Err-поле; без декодера и сети.

1 / 6

Что дальше#

Тестовая пирамида stringsvc стоит на всех четырёх этажах: сервис и endpoint — table-driven, обёртки — на стабах, транспорт — на httptest. Всё, что написано в уроках 02-15, теперь проверяемо изоляционно, и это последний аргумент в пользу слоистой архитектуры из урока 01-filosofiya: слой тестируется по контракту соседа, а не по контракту мира. Дальше карта курса ведёт в стройку: урок 17-struktura — структура go kit проекта (каталоги, DI, шаблон большого сервиса), затем конфигурация и graceful shutdown; обе темы опираются на матрёшку и endpoint-цепочку из уроков 14-15, которые ты теперь умеешь и писать, и тестировать. Путь до итогового проекта урока 25 — на странице курса; HTTP-декодер для тестов освежи в уроке 04-http-transport.

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

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

Вывод

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

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

  • Пирамида тестов go-kit: сервис и endpoint — table-driven микросекунды, middleware — стабы, кодеки — Recorder, транспорт — httptest.NewServer; дорогое — наверху и редко.
  • Endpoint-тест — прямой вызов ep(ctx, request): проверяется шов адаптера, Failer и перенос ErrEmpty; ошибки — только через errors.Is на sentinel.
  • Middleware тестируется fake-next: сигнальный стаб с panic ловит пролетевший вызов, записывающий — проверяет трансформеры; логгер-стаб из одного метода Log даёт проверку текста логов.
  • Декораторы тестируются fake-сервисом со счётчиком вызовов; матрёшку в тесте собирает та же applyMiddleware, что и prod-код.
  • Recorder-тест прогоняет kithttp.NewServer без сети: статусы, тела и заголовки проверяются на httptest.NewRecorder; sentinel-ветки encodeError — обязательная таблица.
  • Интеграция — httptest.NewServer с клиентом и дедлайном: полный HTTP-цикл и контракт глазами клиента, один-два теста на сервис.
  • Не тестируй библиотеку и не спи в тестах: go-kit остаётся чёрным ящиком, время сжимается, события дожидаются каналом, CI гоняет -race -count=1.

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

Построй тестовую пирамиду stringsvc полностью. Первое: endpoint-тест makeUppercaseEndpoint из урока с тремя кейсами — обычная строка, пустая, строка из пробелов; проверь sentinel через errors.Is. Второе: логгер-стаб из урока и два теста recoveryMiddleware — паника превращается в err с записью в лог, здоровый путь прозрачен; прогон с -race обязателен. Третье: fake-сервис со счётчиком и три утверждения кэша из урока 15 — значения равны, ошибки не кэшируются, base вызван один раз; добавь кейс на два разных ключа. Четвёртое: Recorder-таблица на три кейса — 200 на GO, 400 на ErrEmpty, 400 на кривой JSON — и проверка Content-Type на положительном кейсе. Пятое: интеграционный тест на httptest.NewServer с дедлайном контекста и defer Close, декодирование ответа в UppercaseResponse. Шестое: замерь время прогона go test -race -count=1 и запиши, какой этаж съел больше всего; если транспортный тест дольше ста миллисекунд — разберись, что мешает. Седьмое: письменно ответь, почему limiter-тест со Sleep в пятьдесят миллисекунд — плохой тест, и как его переписать.

Личные заметкиПусто
Хранятся только в вашем браузере (go-kit · Тестирование endpoints)

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

Нужны ли библиотеки вроде gomock или testify для тестов go-kit?

Нет, стандартная библиотека закрывает почти всё: endpoint — функция, middleware — функция над функцией, сервис — интерфейс из двух методов; ручные стабы по двадцать строк нагляднее кодогенерации. gomock уместен, когда интерфейсов десятки и стабы разрастаются; testify добавляет assert-хелперы поверх t.Fatal. Канон курса — таблицы на struct-литералах, стабы на замыканиях, -race в CI: минимум зависимостей, максимум читаемости.

Как тестировать gRPC-транспорт из урока 13?

Тем же паттерном, что и HTTP: каркас grpc-go даёт тестовый канал bufconn — gRPC-сервер слушает не порт, а буфер, и клиент подключается через grpc.DialContext с Dialer на тот же буфер; сеть и порты не нужны. Endpoint-тесты при этом те же: gRPC-кодеки — функции, grpctrans.NewServer — обёртка endpoint'а, и половина проверок делается до транспорта. Интеграцию же гоняют по bufconn с дедлайном контекста.

Тест endpoint или тест сервиса — не дубль ли это?

Нет: сервисный тест проверяет логику (uppercase и sentinel), endpoint-тест — шов: type assertion request-структуры, упаковку ответа, перенос ошибки в Err-поле через Failer. Ломаются эти этажи по-разному: рефакторинг сервиса не должен ронять тесты адаптера и наоборот. Плюс endpoint-тест принимает декорированный сервис — он ловит поломки интеграции этажей из уроков 14-15, которые сервисный тест не видит в принципе.

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

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

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

gRPC25 уроков~14 чпродвинутый25 уроков уже доступны

gRPC-Go: высокопроизводительный RPC

Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.

  • Писать .proto-схемы и генерировать код
  • Реализовывать unary и streaming сервисы
  • Управлять метаданными, дедлайнами и отменами
  • Строить interceptors для логов и аутентификации