Приветствую, в этой статье я расскажу вам, как мы можем увеличить тестовую производительность наших HTTP-серверов в проектах, которые мы разработали с помощью Go.

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

Теперь давайте кратко рассмотрим содержание и перейдем к деталям нашего предмета.

Содержание

  • Go HTTP Internals
    — http.ListenAndServe
    — net.Conn & net.Listener
    — http.Client Dial Function
    — net.Pipe
    Тестирование в разных пакетах
  • Fiber
    — app.Test Internals
    — app.ServeConn
  • fasthttp
    — fasthttputil.NewInmemoryListener
  • Переход на HTTP
    — httptest.NewUnstartedServer
    — Пример имитации InMemListener
  • Наше улучшение производительности тестов
  • Заключение

Переходите на HTTP-внутренности

Давайте посмотрим, что нам нужно сделать, когда мы хотим запустить простой HTTP-сервер с языком Go.

http.ListenAndServe

Когда мы хотим запустить сервер, который мы написали с использованием стандартной библиотеки Go, мы используем метод ListenAndServe.

Давайте посмотрим на детали метода ListenAndServe:

Теперь давайте посмотрим на детали метода server.ListenAndServe():

Наконец, давайте рассмотрим детали метода srv.Serve(ln). Я удалил часть кода, поэтому он не слишком длинный.

Часть, на которую мы должны обратить внимание, — это метод l.Accept.

Краткое изложение процесса, выполненного в кодах, которыми я поделился до сих пор, на самом деле:

  • Метод http.ListenAndServe создает сервер в фоновом режиме, и запускается метод сервера ListenAndServe.
  • Сервер создает объект net.Listener, который принимает запрос от порта, указанного вызовом net.Listen("tcp", addr)method, и запускает метод Serve.
  • Метод Serve воздействует на объект net.Conn, представляющий новые входящие подключения, с помощью метода Accept() объекта net.Listener, который он получает в качестве параметра.

До сих пор мы видели процесс создания API в его базовой форме.

Теперь я слышу, как вы медленно говорите: «Ну, сэр, что они будут делать в реальной жизни?»

Не волнуйтесь, скоро мы увидим, как мы можем улучшить производительность тестирования, используя эту информацию.

net.Listen и net.Conn

Теперь давайте подробнее рассмотрим эти два основных типа, включенных в пакет HTTP.

Мы видим, что эти два типа на самом деле являются интерфейсами.

Что это значит? Мы можем дать специальные структуры, реализующие эти интерфейсы в качестве параметров для методов, которые ожидают эти типы интерфейсов в качестве параметров. Это важный момент для нас.

http.Клиентская функция набора номера

Теперь давайте посмотрим, как используется объект http.Client, который позволяет нам делать HTTP-запросы с помощью Go.

Существует два метода отправки запросов с помощью пакета Go HTTP.

  • Первый способ заключается в непосредственном использовании методов http.Get http.Post.
  • Второй способ заключается в использовании объекта http.Client resp, _ := http.Get("http://localhost:8080/test").

Фактически, первый метод, http.Get, также использует объект http.Client в фоновом режиме.

Давайте кратко рассмотрим:

Другими словами, оба метода делают одно и то же в фоновом режиме.

Теперь давайте посмотрим на поля, которые мы можем настроить при создании объекта http.Client.

http.Client предоставляет все процессы установления соединения с интерфейсом RoundTripper. И он использует объект Transport как саму реализацию RoundTripper.

Если мы хотим, мы можем сами создать этот объект Transport при создании http.Client.

Созданный нами транспортный объект имеет метод с именем DialContext. Этот метод позволяет открывать соединение в фоновом режиме. Он возвращает значение типа net.Conn в качестве переменной.

Через мгновение мы объединим эту информацию и информацию о том, что метод server.Serve, который мы только что видели, прослушивает соединения с net.Conn, используя метод accept переменной net.Listener, которая принимается в качестве параметра.

Постепенно части встают на свои места, не так ли?

сеть.труба

Наконец, есть еще один метод, о котором я хотел бы упомянуть: net.Pipe(). Он возвращает две переменные типа net.Conn в качестве возвращаемого значения.

Одна из этих переменных может использоваться как точка записи, а другая — как точка чтения. Итак, что это значит? Как мы можем это использовать?

Если вы помните, метод Listen() объекта Listener возвращал переменную net.Conn. Метод Dial() объекта HTTP-клиента Transport также возвращает переменную net.Conn.

Начиная с этого момента, когда мы передаем один конец соединений, возвращаемых методом net.Pipe(), методу Dial нашего HTTP-клиента, а другой конец — Listen нашего объекта Listener запрос, который мы сделали с HTTP-клиентом, создаст соединение, ожидаемое прослушивателем.

Тестирование в разных пакетах

Теперь давайте создадим API, используя несколько различных библиотек веб-приложений, распространенных в языке Go, и напишем для них тесты.

Волокно

Во-первых, давайте взглянем на библиотеку Fiber, которую мы часто используем в наших проектах и ​​в которую я вношу свой вклад.

Написать тест с помощью Fiber достаточно просто, так как он содержит прямой метод Test.

Теперь давайте посмотрим на пример приложения Fiber и на то, как оно было протестировано:

Здесь нам нужно обратить внимание на строку app.Test(r, -1). Мы отправляем наш HTTP-запрос сюда, чтобы мы могли его протестировать.

Так как же этот HTTP-запрос прошел без вызоваapp.Listen(":8080"), который мы обычно делаем, чтобы запустить приложение Fiber и заставить его прослушивать порт?

Давайте посмотрим, что стоит за методом app.Test() (вы можете получить доступ ко всему коду здесь):

Когда мы изучаем код, мы видим, что Fiber фактически создает свои собственные объекты соединения с помощью conn := new(testConn) для тестирования приложения, как мы объясняли в разделе net.Pipe(). Он записывает HTTP-запрос в этот объект подключения и отправляет этот объект подключения на сервер с вызовом app.server.ServeConn(conn).

Так что же происходит, когда мы обычно говорим app.Listen(":8080") в приложении Fiber? Давайте быстро взглянем на это:

Здесь мы видим, что Fiber выполняет операцию net.Listen() внутри себя и передает созданный им объект слушателя серверу, говоря app.server.Serve(ln). Сервер прослушивает подключения от этого объекта прослушивателя и передает его в качестве параметра методу serveConn().

Другими словами, он реализует подход, объединяющий объекты Listener и net.Conn, которые мы объяснили в начале, что позволяет нам протестировать его без физического прослушивания порта.



быстрыйhttp

Мы сказали, что Fiber использует пакет Fasthttp в фоновом режиме. Если мы хотим, мы можем разрабатывать высокопроизводительные API-приложения напрямую, используя пакет Fasthttp.

Теперь давайте посмотрим, как мы можем протестировать приложение, которое мы разработали с помощью Fasthttp.

В приведенном выше коде мы создали простой сервер и определили обработчик, а когда был сделан запрос GET к конечной точке /test, мы вернули «ОК ответ.

Здесь, в отличие от предыдущего кода, мы видим такое определение, как ln := fasthttputil.NewInmemoryListener().

Затем мы определяем http.Client и передаем метод Dial созданной нами переменной ln методу DailContext объекта Transport. объект.

Так тебе это ничего не напоминает? Да, мы упоминали выше, что мы можем передать объект net.Listener в нашу переменную http.Client и протестировать его в памяти. Fasthttp использует именно этот подход с пакетом утилит в фоновом режиме.

Те, кому интересны подробности кода, могут прочитать его здесь.



Перейти на HTTP

Теперь давайте напишем тест для API, который мы разработали с помощью собственных пакетов Go без использования каких-либо внешних библиотек.

Допустим, мы разработали API, как описано выше. Итак, как мы это проверим?

Мы также можем написать наши тесты, используя методы, которые мы изучили здесь выше. В языке Go есть пакет под названием httptest, который содержит несколько служебных методов. В этом пакете метод с именем NewUnstartedServer() возвращает незапущенный объект тестового сервера.

Давайте посмотрим на исходный код:

Когда мы создаем сервер с помощью метода httptest.NewUnstartedServer(), в фоновом режиме создается прослушиватель, который прослушивает случайный порт с помощью внутреннего метода newLocalListener().

Когда мы говорим start, он прослушивает подключения от прослушивателя в горутине с помощью метода s.goServe().

Что приходит на ум, когда мы видим Listener? Как и в Fasthttp, если мы создадим InMemoryListener и передадим его нашему серверу, мы сможем выполнить наш тест, не прослушивая физический порт.

В приведенном выше коде мы запускаем наш сервер, фактически прослушивая физический порт в тесте с именем TestHttpServer. Я поставил time.Sleep, чтобы дождаться запуска сервера. Используя метод srv.Listener.Addr().String(), мы можем получить адрес (включая случайно назначенный ОС порт), который нам нужно запросить.

В тесте под названием TestHttpServerInMemory мы выполнили HTTP-связь между сервером и клиентом через созданный нами прослушиватель в памяти, не прослушивая физический порт.

Здесь я использовал метод fasthttputil.NewInMemoryListener для удобства. Если мы хотим, мы можем сделать то же самое, создав структуру и реализовав интерфейс Listener.

Давайте быстро взглянем на это:

Как мы видели выше, мы можем обеспечить связь между сервером и клиентом в памяти, самостоятельно реализовав интерфейс Listener и используя метод net.Pipe(). Причина, по которой я передал объекты соединения нашей структуре InMemListener по каналу, заключается в том, что сервер вызывает метод Accept() в цикле и ожидает нового соединения. Таким образом, только если мы дадим соединение нашему объекту Listener, серверная сторона получит новое соединение.

Так почему же Go не предоставляет этот подход с сервером в памяти по умолчанию?

На самом деле, есть своевременный выпуск по этому поводу, и некоторые люди работали над этим, но процесс еще не завершен.



Наши улучшения производительности тестов

Теперь давайте посмотрим, как мы улучшили производительность собственных тестов, используя описанные выше методы.

Мы разработали наше приложение с помощью fasthttp, и это приложение действовало как базовый обратный прокси-сервер. Мы также используем некоторые функции, предоставляемые fasthttp для работы обратного прокси.

Для наших тестов нам нужен обратный прокси-сервер (наше приложение) и поддельный API для представления сервера, который мы перенаправляем в фоновом режиме.

Для этого мы использовали httptest.NewUnstartedServer и fasthttptest.NewInmemoryListener.

Раньше мы фактически прослушивали порт для запуска тестовых серверов в наших тестах, и это немного замедляло наш процесс тестирования. Когда я посмотрел на наш конвейер CI, наш процесс тестирования занял около 10 секунд.

В результате нашей разработки, после использования подхода прослушивателя в памяти, все наши тесты были завершены за ~ 0,3 секунды.

На изображении ниже мы можем видеть все наши тесты и общее время безотказной работы.

Заключение

  • Когда мы хотим протестировать HTTP-сервер, мы можем выполнить наш тест быстрее, не прослушивая физический порт.
  • Наши тесты работают намного быстрее с этой техникой
  • Мы можем имитировать сетевые операции в памяти, используя net.Pipe(), net.Listener, net.Conn
  • Мы можем использовать метод app.Test() при тестировании наших приложений с помощью волокна.
  • Мы можем создать прослушиватель в памяти, используя fasthttputil.InMemoryListener().

Надеюсь, эта статья была для вас интересной и познавательной. Увидимся в следующем посте :)

Давайте наладим: