Методы тестирования `test::nginx`: конфигурация, отправка запросов и обработка ответов
API7.ai
November 18, 2022
В предыдущей статье мы уже получили первое представление о test::nginx и запустили самый простой пример. Однако в реальных проектах с открытым исходным кодом тестовые случаи, написанные на test::nginx, гораздо сложнее и труднее для освоения, чем примеры кода. Иначе это не называлось бы препятствием.
В этой статье я проведу вас через часто используемые команды и методы тестирования в test::nginx, чтобы вы могли понять большинство наборов тестовых случаев в проекте OpenResty и получить возможность писать более реалистичные тестовые случаи. Даже если вы еще не вносили вклад в код OpenResty, знакомство с тестовым фреймворком OpenResty станет отличным вдохновением для вас при проектировании и написании тестовых случаев в вашей работе.
Тестирование test::nginx по сути генерирует nginx.conf и запускает процесс NGINX на основе конфигурации каждого тестового случая. Затем оно имитирует клиентский запрос с указанным телом запроса и заголовками. Далее код Lua в тестовом случае обрабатывает запрос и формирует ответ. В этот момент test::nginx анализирует критическую информацию, такую как тело ответа, заголовки ответа и логи ошибок, и сравнивает их с тестовой конфигурацией. Если есть расхождение, тест завершается с ошибкой; в противном случае он считается успешным.
test::nginx предоставляет множество примитивов DSL (Domain-specific language). Я сделал простую классификацию в соответствии с настройкой NGINX, отправкой запросов, обработкой ответов и проверкой логов. Эти 20% функциональности покрывают 80% сценариев применения, поэтому мы должны твердо их освоить. Что касается других более продвинутых примитивов и их использования, мы представим их в следующей статье.
Конфигурация NGINX
Давайте сначала рассмотрим конфигурацию NGINX. Примитив test::nginx с ключевым словом "config" связан с конфигурацией NGINX, например, config, stream_config, http_config и т.д.
Их функции одинаковы: вставить указанную конфигурацию NGINX в различные контексты NGINX. Эти конфигурации могут быть либо командами NGINX, либо кодом Lua, заключенным в content_by_lua_block.
При выполнении модульных тестов config является наиболее часто используемым примитивом, в котором мы загружаем библиотеки Lua и вызываем функции для тестирования "белого ящика". Вот фрагмент тестового кода, который не может быть полностью запущен. Он взят из реального проекта с открытым исходным кодом, поэтому, если вам интересно, вы можете перейти по ссылке, чтобы увидеть полный тест, или попробовать запустить его локально.
=== TEST 1: sanity --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.key-auth") local ok, err = plugin.check_schema({key = 'test-key'}) if not ok then ngx.say(err) end ngx.say("done") } }
Цель этого тестового случая — проверить, правильно ли работает функция check_schema в файле кода plugins.key-auth. В нем используется команда NGINX content_by_lua_block в location /t, чтобы загрузить тестируемый модуль и напрямую вызвать функцию, которую нужно проверить.
Это распространенный метод тестирования "белого ящика" в test::nginx. Однако этой конфигурации недостаточно для завершения теста, поэтому давайте продолжим и посмотрим, как отправить клиентский запрос.
Отправка запросов
Имитация отправки запроса клиентом включает множество деталей, поэтому начнем с самой простой — отправки одного запроса.
request
Продолжая с вышеуказанным тестовым случаем, если мы хотим, чтобы код модульного теста был выполнен, то мы должны инициировать HTTP-запрос на адрес /t, указанный в конфигурации, как показано в следующем тестовом коде:
--- request GET /t
Этот код отправляет GET-запрос на /t в примитиве запроса. Здесь мы не указываем IP-адрес, доменное имя или порт доступа, а также не указываем, используется ли HTTP 1.0 или HTTP 1.1. Все эти детали скрыты test::nginx, поэтому нам не нужно о них заботиться. Это одно из преимуществ DSL — нам нужно сосредоточиться только на бизнес-логике, не отвлекаясь на все детали.
Кроме того, это обеспечивает частичную гибкость. Например, по умолчанию используется протокол HTTP 1.1, или если мы хотим протестировать HTTP 1.0, мы можем указать это отдельно:
--- request GET /t HTTP/1.0
Помимо метода GET, также необходимо поддерживать метод POST. В следующем примере мы можем отправить строку hello world на указанный адрес.
--- request POST /t hello world
Опять же, test::nginx автоматически вычисляет длину тела запроса и добавляет заголовки запроса host и connection, чтобы гарантировать, что это нормальный запрос.
Конечно, мы можем добавить комментарии, чтобы сделать код более читаемым. Строки, начинающиеся с #, будут распознаны как комментарии к коду.
--- request # post request POST /t hello world
Запрос также поддерживает более сложный и гибкий режим, который использует eval в качестве фильтра для непосредственного встраивания кода Perl, поскольку test::nginx написан на Perl. Если текущий язык DSL не удовлетворяет вашим потребностям, eval — это "последний аргумент" для выполнения кода Perl напрямую.
Для использования eval рассмотрим несколько простых примеров, а в следующей статье продолжим с другими, более сложными.
--- request eval "POST /t hello\x00\x01\x02 world\x03\x04\xff"
В первом примере мы используем eval для указания непечатаемых символов, что является одним из его применений. Содержимое между двойными кавычками будет обработано как строка Perl и затем передано в request в качестве аргумента.
Вот более интересный пример:
--- request eval "POST /t\n" . "a" x 1024
Однако, чтобы понять этот пример, нам нужно знать кое-что о строках в Perl, поэтому я кратко упомяну два момента.
- В Perl мы используем точку для обозначения конкатенации строк. Не напоминает ли это вам две точки в
Lua? - Строчная буква
xуказывает количество повторений символа. Например,"a" x 1024выше означает, что символ "a" повторяется 1024 раза.
Таким образом, второй пример означает, что метод POST отправляет запрос, содержащий 1024 символа a, на адрес /t.
pipelined_requests
После понимания того, как отправить один запрос, давайте рассмотрим, как отправить несколько запросов. В test::nginx мы можем использовать примитив pipelined_requests для отправки нескольких запросов последовательно в рамках одного соединения keep-alive:
--- pipelined_requests eval ["GET /hello", "GET /world", "GET /foo", "GET /bar"]
Например, этот пример будет последовательно обращаться к этим четырем API в одном соединении. Это имеет два преимущества:
- Первое — можно устранить множество повторяющегося тестового кода, и четыре тестовых случая можно сжать в один.
- Второе и самое важное — мы можем использовать pipelined-запросы для обнаружения, будет ли логика кода вызывать исключения в случае множественных обращений.
Вы можете задаться вопросом: если я напишу несколько тестовых случаев последовательно, то код также будет выполнен несколько раз на этапе выполнения. Разве это не покрывает вторую проблему выше?
Это связано с режимом выполнения test::nginx, который работает не так, как вы могли бы подумать. После каждого тестового случая test::nginx завершает текущий процесс NGINX, и все данные в памяти исчезают. При запуске следующего тестового случая nginx.conf генерируется заново, и запускается новый Worker NGINX. Этот механизм гарантирует, что тестовые случаи не влияют друг на друга.
Поэтому, когда мы хотим протестировать несколько запросов, нам нужно использовать примитив pipelined_requests. На его основе мы можем моделировать ограничение скорости, ограничение параллелизма и многие другие сценарии, чтобы проверить, правильно ли работает ваша система в более реалистичных и сложных сценариях. Мы оставим это для следующей статьи, так как это будет связано с несколькими командами и примитивами.
repeat_each
Мы только что упомянули случай тестирования нескольких запросов, так как же выполнить один и тот же тест несколько раз?
Для этой проблемы test::nginx предоставляет глобальную настройку: repeat_each, которая является функцией Perl и по умолчанию имеет значение repeat_each(1), что означает, что тестовый случай будет выполнен только один раз. Поэтому в предыдущих тестовых случаях мы не утруждаем себя отдельной настройкой.
Естественно, мы можем установить ее перед функцией run_test(), например, изменив аргумент на 2.
repeat_each(2); run_tests();
Тогда каждый тестовый случай будет выполнен дважды, и так далее.
more_headers
После обсуждения тела запроса давайте рассмотрим заголовки запроса. Как мы упоминали выше, test::nginx по умолчанию отправляет запрос с заголовками host и connection. А как насчет других заголовков запроса?
more_headers специально предназначен для этого.
--- more_headers X-Foo: blah
Мы можем использовать его для установки различных пользовательских заголовков. Если мы хотим установить более одного заголовка, то указываем более одной строки:
--- more_headers X-Foo: 3 User-Agent: openresty
Обработка ответов
После отправки запроса наиболее важной частью test::nginx является обработка ответа, где мы будем определять, соответствует ли ответ ожиданиям. Здесь мы разделим его на четыре части и представим их: тело ответа, заголовок ответа, код состояния ответа и логи.
response_body
Аналогом примитива запроса является response_body, и ниже приведен пример их двух конфигураций в использовании:
=== TEST 1: sanity --- config location /t { content_by_lua_block { ngx.say("hello") } } --- request GET /t --- response_body hello
Этот тестовый случай будет пройден, если тело ответа — hello, и будет сообщено об ошибке в других случаях. Но как мы тестируем длинное тело ответа? Не беспокойтесь, test::nginx уже позаботился об этом для вас. Он поддерживает обнаружение тела ответа с помощью регулярного выражения, как в следующем примере:
--- response_body_like ^he\w+$
Это позволяет вам быть очень гибкими с телом ответа. Кроме того, test::nginx также поддерживает операции unlike:
--- response_body_unlike ^he\w+$
В этом случае, если тело ответа — hello, тест не будет пройден.
По аналогии, после понимания обнаружения одного запроса, давайте рассмотрим обнаружение нескольких запросов. Вот пример использования с pipelined_requests:
--- pipelined_requests eval ["GET /hello", "GET /world", "GET /foo", "GET /bar"] --- response_body eval ["hello", "world", "oo", "bar"]
Конечно, важно отметить, что сколько запросов вы отправляете, столько ответов вам нужно для соответствия.
response_headers
Во-вторых, поговорим о заголовке ответа. Заголовок ответа аналогичен заголовку запроса в том, что каждая строка соответствует ключу и значению заголовка.
--- response_headers X-RateLimit-Limit: 2 X-RateLimit-Remaining: 1
Как и обнаружение тела ответа, заголовки ответа также поддерживают регулярные выражения и операции unlike, такие как response_headers_like, raw_response_headers_like и raw_response_headers_unlike.
error_code
Третий — это код ответа. Обнаружение кода ответа поддерживает прямое сравнение и также поддерживает операции like, как в следующих двух примерах:
--- error_code: 302
--- error_code_like: ^(?:500)?$
В случае нескольких запросов error_code нужно проверять несколько раз:
--- pipelined_requests eval ["GET /hello", "GET /hello", "GET /hello", "GET /hello"] --- error_code eval [200, 200, 503, 503]
error_log
Последний тестовый элемент — это лог ошибок. В большинстве тестовых случаев лог ошибок не генерируется. Мы можем использовать no_error_log для обнаружения:
--- no_error_log [error]
В приведенном выше примере, если строка [error] появляется в error.log NGINX, тест завершится неудачей. Это очень распространенная функция, и рекомендуется добавлять обнаружение лога ошибок во все ваши нормальные тесты.
--- error_log hello world
Приведенная выше конфигурация обнаруживает наличие hello world в error.log. Конечно, вы можете использовать eval, встроенный в код Perl, для реализации обнаружения с помощью регулярных выражений, как в следующем примере:
--- error_log eval qr/\[notice\] .*? \d+ hello world/
Итог
Сегодня мы изучаем, как отправлять запросы и тестировать ответы в test::nginx, включая тело запроса, заголовок, код состояния ответа и лог ошибок. Мы можем реализовать полный набор тестовых случаев с комбинацией этих примитивов.
Наконец, вот вопрос для размышления: Каковы преимущества и недостатки абстрактного DSL test::nginx? Не стесняйтесь оставлять комментарии и обсуждать со мной, а также вы можете поделиться этой статьей, чтобы общаться и думать вместе.