`test::nginx`의 잘 알려지지 않은 사용법
API7.ai
November 24, 2022
이전 두 글에서 여러분은 test::nginx
의 대부분의 사용법을 익혔으며, OpenResty 프로젝트의 테스트 케이스 세트를 이해할 수 있을 것이라고 믿습니다. 이는 OpenResty와 그 주변 라이브러리를 학습하는 데 충분합니다.
하지만 OpenResty 코드 기여자가 되고 싶거나, 프로젝트에서 test::nginx
를 사용하여 테스트 케이스를 작성하려는 경우, 더 고급이고 복잡한 사용법을 배워야 합니다.
오늘의 글은 아마도 이 시리즈에서 가장 "인기 없는" 부분일 것입니다. 왜냐하면 이전에 아무도 공유하지 않았던 내용이기 때문입니다. OpenResty의 핵심 모듈인 lua-nginx-module
을 예로 들면, 전 세계적으로 70명 이상의 기여자가 있지만, 모든 기여자가 테스트 케이스를 작성한 것은 아닙니다. 따라서 오늘의 글을 읽는다면, 여러분의 test::nginx
에 대한 이해는 전 세계 상위 100위 안에 들 것입니다.
테스트에서의 디버깅
먼저, 개발자가 일반적으로 디버깅할 때 사용하는 가장 간단하고 일반적인 섹션을 살펴보겠습니다. 여기서는 이러한 디버깅 관련 섹션의 사용 시나리오를 하나씩 소개하겠습니다.
ONLY
종종, 우리는 기존 테스트 케이스 세트에 새로운 테스트 케이스를 추가합니다. 테스트 파일에 많은 테스트 케이스가 포함되어 있다면, 이를 실행하는 데 시간이 많이 소요되며, 특히 테스트 케이스를 반복적으로 수정해야 할 때 더욱 그렇습니다.
그렇다면, 지정한 테스트 케이스 중 하나만 실행할 수 있는 방법이 있을까요? 이는 ONLY
섹션을 통해 쉽게 할 수 있습니다.
=== TEST 1: sanity
=== TEST 2: get
--- ONLY
위의 의사 코드는 이 섹션을 사용하는 방법을 보여줍니다. 단독으로 실행해야 하는 테스트 케이스의 마지막 줄에 --- ONLY
를 넣으면, prove
를 사용하여 테스트 케이스 파일을 실행할 때 다른 모든 테스트 케이스는 무시되고 이 테스트만 실행됩니다.
그러나 이는 디버깅 중일 때만 적합합니다. 따라서 prove
명령어가 ONLY
섹션을 발견하면, 코드를 커밋할 때 이를 제거하지 않도록 알려줍니다.
SKIP
하나의 테스트 케이스만 실행하는 요구 사항에 대응하여, 특정 테스트 케이스를 무시하는 요구 사항이 있습니다. SKIP
섹션은 일반적으로 아직 구현되지 않은 기능을 테스트할 때 사용됩니다:
=== TEST 1: sanity
=== TEST 2: get
--- SKIP
이 의사 코드에서 볼 수 있듯이, 그 사용법은 ONLY
와 유사합니다. 우리는 테스트 주도 개발을 하기 때문에, 먼저 테스트 케이스를 작성해야 합니다. 그리고 구현을 공동으로 코딩할 때, 구현의 난이도나 우선순위 때문에 기능의 구현을 지연시킬 수 있습니다. 그러면 해당 테스트 케이스 세트를 먼저 건너뛰고, 구현이 완료되면 SKIP
섹션을 제거할 수 있습니다.
LAST
또 다른 일반적인 섹션은 LAST
입니다. 이는 사용법이 간단하며, 이전의 테스트 케이스는 실행되고 이후의 테스트 케이스는 무시됩니다.
=== TEST 1: sanity
=== TEST 2: get
--- LAST
=== TEST 3: set
여러분은 궁금할 수 있습니다. ONLY
와 SKIP
의 중요성은 이해하겠지만, LAST
의 사용처는 무엇일까요? 사실, 때로는 테스트 케이스 간에 의존성이 있어, 후속 테스트가 의미를 가지려면 먼저 몇 가지 테스트 케이스를 실행해야 할 수 있습니다. 따라서 이 경우, 디버깅을 계속할 때 LAST
가 매우 유용합니다.
plan
test::nginx
의 모든 기능 중에서 plan
은 가장 짜증나고 이해하기 어려운 기능 중 하나입니다. 이는 Perl의 Test::Plan
모듈에서 파생되었으며, test::nginx
에는 이에 대한 문서가 없고, 설명을 찾기 쉽지 않습니다. 따라서 초반에 이를 소개하겠습니다. 저는 OpenResty 코드 기여자 중 몇 명이 이 함정에 빠져 나오지 못한 것을 보았습니다.
다음은 공식 OpenResty 테스트 세트의 모든 파일 시작 부분에서 볼 수 있는 유사한 구성의 예입니다:
plan tests => repeat_each() * (3 * blocks());
여기서 plan
의 의미는 전체 테스트 파일에서 계획에 따라 몇 개의 테스트를 수행해야 하는지입니다. 최종 실행 결과가 계획과 일치하지 않으면 테스트가 실패합니다.
이 예에서 repeat_each
의 값이 2
이고 테스트 케이스가 10개라면, plan
의 값은 2 x 3 x 10 = 60
이어야 합니다. 유일하게 혼란스러울 수 있는 것은 숫자 3
의 의미입니다. 이는 마치 마법의 숫자처럼 보입니다!
걱정하지 마세요. 계속해서 예제를 살펴보면 곧 이해할 수 있을 것입니다. 먼저, 다음 테스트 케이스에서 plan
의 올바른 값이 무엇인지 알아낼 수 있나요?
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
ngx.say("hello")
}
}
--- request
GET /t
--- response_body
hello
모두 plan = 1
이라고 결론지을 것입니다. 왜냐하면 테스트는 response_body
만 확인하기 때문입니다.
하지만 그렇지 않습니다! 정답은 plan = 2
입니다. 왜냐하면 test::nginx
에는 암묵적인 검사가 있기 때문입니다. 즉, --- error_code: 200
으로, 기본적으로 HTTP 응답 코드가 200
인지 확인합니다.
따라서 위의 마법의 숫자 3
은 실제로 각 테스트가 명시적으로 두 번 검사된다는 것을 의미합니다. 예를 들어 body
와 error log
를 검사하고, 암묵적으로 response code
를 검사합니다.
이것이 너무 오류가 발생하기 쉬우므로, 다음과 같은 방법으로 plan
을 끄는 것을 권장합니다.
use Test::Nginx::Socket 'no_plan';
만약 끌 수 없다면, 예를 들어 공식 OpenResty 테스트 세트에서 부정확한 plan
을 발견했다면, 원인을 깊이 파고들지 말고, 단순히 plan
의 표현식에 숫자를 더하거나 빼는 것을 권장합니다.
plan tests => repeat_each() * (3 * blocks()) + 2;
이것이 공식적으로 사용될 방법입니다.
전처리기
우리는 동일한 테스트 파일의 다른 테스트 케이스 간에 일부 공통 설정이 있을 수 있다는 것을 알고 있습니다. 각 테스트 케이스에서 설정을 반복하면 코드가 중복되고 나중에 수정하기 번거로울 수 있습니다.
이때, add_block_preprocessor
지시어를 사용하여 Perl 코드를 추가할 수 있습니다. 예를 들어 다음과 같습니다:
add_block_preprocessor(sub {
my $block = shift;
if (!defined $block->config) {
$block->set_value("config", <<'_END_');
location = /t {
echo $arg_a;
}
_END_
}
});
이 전처리기는 모든 테스트 케이스에 config
섹션을 추가하며, 내용은 location /t
입니다. 따라서 이후 테스트 케이스에서 config
를 생략하고 직접 접근할 수 있습니다.
=== TEST 1:
--- request
GET /t?a=3
--- response_body
3
=== TEST 2:
--- request
GET /t?a=blah
--- response_body
blah
사용자 정의 함수
전처리기에 Perl 코드를 추가하는 것 외에도, run_tests
함수 전에 임의의 Perl 함수 또는 사용자 정의 함수를 추가할 수 있습니다.
다음은 파일을 읽는 함수를 추가하고 eval
지시어와 결합하여 POST
파일을 구현하는 예입니다:
sub read_file {
my $infile = shift;
open my $in, $infile
or die "cannot open $infile for reading: $!";
my $content = do { local $/; <$in> };
close $in;
$content;
}
our $CONTENT = read_file("t/test.jpg");
run_tests;
__DATA__
=== TEST 1: sanity
--- request eval
"POST /\n$::CONTENT"
셔플
위의 내용 외에도, test::nginx
에는 잘 알려지지 않은 함정이 있습니다: 기본적으로 테스트 케이스를 무작위 순서로 실행합니다. 테스트 케이스의 순서와 번호를 따르지 않습니다.
이는 처음에 더 많은 문제를 테스트하기 위한 것이었습니다. 결국 각 테스트 케이스가 실행된 후 NGINX 프로세스가 종료되고, 새로운 NGINX 프로세스가 시작되어 실행되므로 결과는 순서와 관련이 없어야 합니다.
저수준 프로젝트의 경우 이는 사실입니다. 그러나 애플리케이션 수준 프로젝트의 경우, 외부에 데이터베이스와 같은 지속적 저장소가 존재합니다. 무작위 실행은 잘못된 결과를 초래할 수 있습니다. 매번 무작위이기 때문에, 오류가 발생할 수도 있고 발생하지 않을 수도 있으며, 오류가 매번 다를 수 있습니다. 이는 개발자들에게 혼란을 초래하며, 저도 여러 번 이 함정에 빠졌습니다.
따라서, 제 조언은: 이 기능을 끄십시오. 다음 두 줄의 코드로 이를 끌 수 있습니다:
no_shuffle();
run_tests;
특히, no_shuffle
은 무작위화를 비활성화하고 테스트가 테스트 케이스의 순서에 따라 엄격하게 실행되도록 합니다.
reindex
OpenResty의 테스트 케이스 세트는 엄격한 형식 요구 사항이 있습니다. 각 테스트 케이스는 세 개의 줄바꿈으로 구분되어야 하며, 테스트 케이스 번호는 엄격하게 자동 증가해야 합니다.
다행히도, 우리는 이러한 지루한 작업을 자동으로 수행하는 도구인 reindex
가 있습니다. 이는 openresty-devel-utils 프로젝트에 숨겨져 있습니다. 이에 대한 문서가 없기 때문에, 이를 아는 사람은 거의 없습니다.
관심이 있다면, 테스트 케이스 번호를 엉망으로 만들거나 줄바꿈 수를 추가하거나 제거한 후 이 도구를 사용하여 정리하고 복원할 수 있는지 확인해 보세요.
요약
이것으로 test::nginx
에 대한 소개를 마칩니다. 물론, 더 많은 기능이 있지만, 우리는 핵심적이고 가장 중요한 것들만 이야기했습니다. "한 사람에게 물고기를 주면 하루를 먹여 살릴 수 있지만, 물고기 잡는 법을 가르치면 평생을 먹여 살릴 수 있습니다." 저는 여러분에게 테스트 학습의 기본 방법과 주의 사항을 가르쳤습니다. 이제 공식 테스트 케이스 세트를 파고들어 더 나은 이해를 얻을 수 있습니다.
마지막으로, 아래 질문에 대해 생각해 보세요. 프로젝트 개발에서 테스트가 있나요? 그리고 어떤 프레임워크를 사용하여 테스트하나요? 이 글을 더 많은 사람들과 공유하여 함께 교류하고 학습할 수 있도록 환영합니다.