`test::nginx`의 잘 알려지지 않은 사용법

API7.ai

November 24, 2022

OpenResty (NGINX + Lua)

이전 두 글에서 여러분은 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

여러분은 궁금할 수 있습니다. ONLYSKIP의 중요성은 이해하겠지만, 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은 실제로 각 테스트가 명시적으로 두 번 검사된다는 것을 의미합니다. 예를 들어 bodyerror 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에 대한 소개를 마칩니다. 물론, 더 많은 기능이 있지만, 우리는 핵심적이고 가장 중요한 것들만 이야기했습니다. "한 사람에게 물고기를 주면 하루를 먹여 살릴 수 있지만, 물고기 잡는 법을 가르치면 평생을 먹여 살릴 수 있습니다." 저는 여러분에게 테스트 학습의 기본 방법과 주의 사항을 가르쳤습니다. 이제 공식 테스트 케이스 세트를 파고들어 더 나은 이해를 얻을 수 있습니다.

마지막으로, 아래 질문에 대해 생각해 보세요. 프로젝트 개발에서 테스트가 있나요? 그리고 어떤 프레임워크를 사용하여 테스트하나요? 이 글을 더 많은 사람들과 공유하여 함께 교류하고 학습할 수 있도록 환영합니다.