`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 명령어일 수도 있고, content_by_lua_block
으로 캡슐화된 Lua 코드일 수도 있습니다.
단위 테스트를 할 때, 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")
}
}
이 테스트 케이스의 목적은 plugins.key-auth
코드 파일의 check_schema
함수가 제대로 작동하는지 테스트하는 것입니다. location /t
에서 content_by_lua_block
NGINX 명령어를 사용하여 테스트할 모듈을 요구하고, 확인할 함수를 직접 호출합니다.
이는 test::nginx
에서 화이트박스 테스트를 수행하는 일반적인 방법입니다. 그러나 이 설정만으로는 테스트를 완료할 수 없으므로, 클라이언트 요청을 어떻게 보내는지 계속해서 살펴보겠습니다.
요청 전송
클라이언트가 요청을 보내는 것을 시뮬레이션하는 것은 상당히 많은 세부 사항이 있으므로, 가장 간단한 것부터 시작하겠습니다 - 단일 요청을 보내는 것입니다.
request
위의 테스트 케이스를 계속해서, 단위 테스트 코드가 실행되려면 config에 지정된 /t
주소로 HTTP 요청을 시작해야 합니다. 다음 테스트 코드와 같습니다:
--- request
GET /t
이 코드는 요청 기본 요소에서 /t
로 GET
요청을 보냅니다. 여기서는 접근 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
문자열을 POST
할 수 있습니다.
--- 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
메서드로 /t
주소에 1024
개의 문자 a
를 포함한 요청을 보내는 것을 의미합니다.
pipelined_requests
단일 요청을 보내는 방법을 이해한 후, 여러 요청을 보내는 방법을 살펴보겠습니다. test::nginx
에서는 pipelined_requests
기본 요소를 사용하여 동일한 keep-alive
연결 내에서 여러 요청을 순차적으로 보낼 수 있습니다:
--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
예를 들어, 이 예제는 동일한 연결에서 이 네 가지 API에 순차적으로 접근합니다. 이에 대한 두 가지 장점이 있습니다:
- 첫째, 많은 반복적인 테스트 코드를 제거할 수 있으며, 네 가지 테스트 케이스를 하나로 압축할 수 있습니다.
- 둘째, 그리고 가장 중요한 이유는, 여러 번 접근할 때 코드 로직에 예외가 발생하는지 여부를 감지할 수 있다는 것입니다.
여러분은 궁금할 수 있습니다. 여러 테스트 케이스를 순차적으로 작성하면, 실행 단계에서 코드도 여러 번 실행됩니다. 위의 두 번째 문제도 커버하지 않나요?
이는 test::nginx
의 실행 모드와 관련이 있습니다. 각 테스트 케이스 후에 test::nginx
는 현재 NGINX 프로세스를 종료하고, 메모리의 모든 데이터가 사라집니다. 다음 테스트 케이스를 실행할 때, nginx.conf
가 다시 생성되고 새로운 NGINX Worker
가 시작됩니다. 이 메커니즘은 테스트 케이스가 서로 영향을 미치지 않도록 보장합니다.
따라서, 여러 요청을 테스트하려면 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]
위의 예제에서 NGINX error.log
에 문자열 [error]
가 나타나면 테스트가 실패합니다. 이는 매우 일반적인 기능이며, 모든 정상 테스트에 오류 로그 감지를 추가하는 것을 권장합니다.
--- error_log
hello world
위의 구성은 error.log
에 hello world
가 있는지 감지합니다. 물론, Perl 코드를 내장한 eval
을 사용하여 정규 표현식 감지를 구현할 수도 있습니다. 예를 들어:
--- error_log eval
qr/\[notice\] .*? \d+ hello world/
요약
오늘 우리는 test::nginx
에서 요청을 보내고 응답을 테스트하는 방법을 배웠습니다. 요청 본문, 헤더, 응답 상태 코드, 오류 로그를 포함합니다. 이러한 기본 요소의 조합으로 완전한 테스트 케이스 세트를 구현할 수 있습니다.
마지막으로, 생각해볼 문제입니다: 추상적인 DSL인 test::nginx
의 장단점은 무엇일까요? 자유롭게 댓글을 남기고 저와 토론해보세요. 또한 이 글을 공유하여 함께 생각을 나누는 것도 환영합니다.