메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

[Perl] 테스트로의 초대

한빛미디어

|

2002-04-11

|

by HANBIT

7,264

저자: chromatic, 역 정직한

언젠가 자신이 프로그램의 유지보수를 모두 떠맡게 될지도 모른다고 생각해보자. 새 기능을 추가해야 할 수도 있고, 오래된 버그를 고쳐야 할 수도 있다. 그리고 그 코드가 자신이 작성한 코드일 수도 있고 다른 누군가가 작성해 알아보기 어려운 혼란스러운 코드일 수도 있다. 다행히도 이런 상황에 맞닥뜨려보지 않았다면 시험삼아 1996 경의 Perl CGI 스크립트를 하나 다운로드 해서 use strict를 사용하고 warning 및 taint 모드로 한 번 돌려보라.

유지보수는 유쾌한 작업과는 거리가 멀고, 변론과 심리학, 그리고 카드로 집 짓는 작업 등이 뒤죽박죽된 일이다. 게다가 여기에 하위호환성, 포팅가능성, 그리고 상호운용성 등까지 끼어들면 일은 더 복잡해 진다. 이런 일에 대한 경험을 쌓고 싶지 않을 수도 있겠지만, 중요한 것들을 배울 수 있는 기회를 놓치지 말기 바란다.
  • 소프트웨어의 개발보다는 유지보수에 더 오랜 시간이 투입된다.
  • 개발 당시에는 명확했던 것이 시간이 흐를수록 애매해 진다.
  • 버그를 고치는 것보다는 그 버그를 찾아내는 일이 더 어려운 일이며 설령 버그를 고쳤다고 하더라도 다른 곳에 영향을 끼치지 않으리라는 것을 확인하는 것은 더 어려운 일이다.
  • 소프트웨어에 투입된 진정한 비용은 코드의 길이라든가 라이센스, 요구되는 하드웨어가 아니라 프로그래머의 시간과 노력이다.
이러한 규칙은 소프트엔지니어링의 초기부터(그레이스 호퍼를 생각해 보라) 잘 정착되어 있다(† 역자 주: 그레이스 호퍼(Grace Hopper)는 컴퓨터의 초창기에 선구적인 역할을 했으며 최초의 컴파일러를 발명해낸 프로그래머다), 그는 다음과 같은 것을 좋은 습관이라 했다. 주석을 잘 달 것! 프로그램의 가정과 논리를 잘 기록해 둘 것! 극한 상황까지 코드를 테스트해보고 또 테스트할 것!

진지한 소프트웨어 공학의 방법론에서는 모두 테스트를 장려한다. 극단 프로그래밍(Extreme Programming)과 같은 새로운 접근방법에서도 테스트는 필수적이다. 포괄적인 일군의 테스트는 코드가 기대하는 대로 동작하는지 검사하는데, 앞으로 수정되어야 할 범위를 최소화 하는데 도움을 준다. 따라서 개발자들을 자유롭게 만들어 코드의 결과에만 매달리지 않고 구조와 설계 개선에 더 많은 시간을 투자할 수 있게 해준다. 테스트를 잘 활용하면 정말로 그렇게 될 수 있다.

주의할 점

풍부한 경험을 쌓은 독자들(특히 수학적인 배경이 탄탄한 사람들)이라면 버그가 없다는 것을 테스트로 증명할 수는 없다는 사실을 바로 알아차렸을 것이다. 약점이 전혀 없으면서 어느 정도 규모가 있는 프로그램을 짠다는 것이 이론적으로 불가능하다면, 그 프로그램이 완벽하게 작동한다는 것을 증명하기 위해 충분한 테스트를 한다는 것도 불가능하다. 어쨌거나 프로그래머는 버그가 없는 코드를 만들기 위해 노력한다. 그렇다면 뭐가 되었든, 가능한 한 모든 것을 테스트해보면 안될 이유가 어디 있겠는가? 테스트를 거친 프로그램에도 알려지지 않은 버그가 있는데, 테스트를 거치지 않은 프로그램은 버그가 분명히 있을 것이다.

물론 어떤 것은 테스트를 시도하는 것 자체가 정말로 불가능하다. 이러한 부류에는 기타 프로세스나 기계, 시스템 라이브러리와 같은 "블랙박스"들이 포함된다. 하지만 테스트할 수 없는 것보다는 테스트할 수 있는 것이 더 많다. 펄은 훌륭한 프로그래머가 흑마술처럼 보일만한 재주를 부릴 수 있도록 해주는데, 여기에는 시스템 테이블과 실행시간 조각 등이 포함된다. 테스트할 수 있는 인터페이스를 충족시키기 위해 자신만의 작은 가상 세계를 만들 수 있는 것이다. 과대망상증처럼 들리겠지만 실제로는 그리 어렵지 않다.

테스트가 쉬운 일은 아니다. 기도와 전역변수, 그리고 동물을 바치는 제사에 의존하고 있는 듯한(† 역자 주: 한 마디로 엉망이란 얘기) 비즈니스 로직에 최소한의 문서화만 되어있는 코드(Perl 4 시절에 설계된)를 한 뭉치 맡게 된다면 생산성을 무지하게 높이게 되거나, 프로그래머들을 어떻게 관리해야 하는지 배워야만 할 것이다. 지금 당장 고치지 않는다면 대체 언제 고치겠다는 말인가? 극단 프로그래밍에서는 테스트할 수 없는 코드는 재작성할 것을 권고한다. 그래야 유지보수와 테스트가 쉽기 때문이다. 때로는 간단한 테스트를 만들어, 코드를 조금 재작업한 후 마음에 드는 결과가 나올 때까지 이 두 가지를 반복해야 한다.

너무 엄청난 일이라고 지레 겁먹을 필요는 없다. 테스트할만한 가치가 있는 펄 코드를 작성할 줄 아는 사람이라면 테스트도 작성할 수 있을 것이다.

펄 모듈 테스팅이 작동하는 법

Perl 제대로 배우기
이 섹션에서는 독자가 이미 perlmodinstall에 익숙하며 모듈을 수동으로 설치할 줄 안다고 가정한다. perl Makefile.PLmake가 끝난 다음, make test를 실행시키면 모듈과 함께 딸려온 테스트들, test.pl 이나 t/ 디렉토리에 있는 "".t"" 로 끝나는 파일들이 수행된다. /blib 디렉토리가 @INC에 추가되며 Test::Harness 가 이 파일들을 실행시키고 그 출력을 캡쳐해서 성공과 실패여부 등의 결과를 짧게 요약해서 보여준다.

테스트의 핵심은 "ok"를 출력하느냐 "not ok"를 출력하느냐 이다. 바로 이것이다. 어떤 테스트 프로그램, 어떤 테스트 프레임워크든 표준출력으로 결과를 내보내는 것이라면 Test::Harness를 사용할 수 있다. 만일 인식론적인 사람이라면(테스트를 훌륭하게 작성하는 사람이 갖추어야 할 특징) "참(truth)이란 무엇일까?"라고 질문할 수도 있다.
        print "1..1\n";

        if (1) {
                print "ok\n";
        } else {
                print "not ok\n";
        }
너무 기초적이라고? 맞다. 가짜 테스트라고? 그렇지는 않다. 위의 테스트는 실제 펄 핵심 테스트의 한 변종이다. 만일 위의 코드를 이해한다면 테스트를 작성할 수 있다는 말이다. 첫번째 줄은 일단 무시한 채 위의 코드를 파일 (truth.t)에 넣은 후 커맨드 라인에서 아래 명령을 실행시키자.
        perl -MTest::Harness -e "runtests "truth.t"";
아니면 아래의 프로그램을 실행시켜도 된다.
        #!/usr/bin/perl -w

        use strict;
        use Test::Harness;

        runtests "truth.t";
이렇게 하면 모든 테스트가 성공했다는 메시지가 출력될 것이다. 만일 그렇지 않다면 뭔가 잘못된 것이고, 많은 사람들이 그 오류를 고치고 싶어할 것이다.

테스트의 첫번째 줄은 첫번째 Test::Harness가 제공하는 간단한 테스트 번호 붙이기 기능이다. Harness 모듈은 전체 테스트의 개수와 테스트 그룹 안에서의 개별 테스트 번호를 알고 싶어한다. 이것은 테스트 작성자에게도 도움이 되는 것이다. 만일 100 개의 테스트 중 하나가 알 수 없는 이유로 실패했다면 디버거를 사용해서 버그를 찾거나, 테스트 슈트의 많은 부분을 주석처리 하거나, 직관적으로 여기저기에 print 문을 넣어서 버그를 찾는 것보다는 93 번 테스트의 위치를 찾는 것이 훨씬 쉽다.

진실을 아는 것은 좋은 일이다. 오류를 구분해 내는 것 역시 좋은 일이다. truth.t를 조금 더 확장해보자. 테스트 번호가 잠재적으로 출력될 수 있는 줄 하나마다 1 씩 증가하는 것을 주의해서 보라. 이것은 명약이 될 수도 있고 독약이 될 수도 있다.
        print "1..2\n";
      
        if (1) {
                print "ok 1\n";
        } else {
                print "not ok 1\n";
        }

        if (0) {
                print "not ok 2\n";
        } else {
                print "ok 2\n";
        }
점점 더 중복이 많아지는 코드는 말할 것도 없고, 테스트 번호를 일관되게 유지하는 것도 힘든 일이다. 잘못된 게으름은 고통스러운 것이다. Test::Harness에서는 실제 테스트의 수가 예상되는 번호와 일치하지 않으면 경고를 내보낸다. 테스트 작성자는 신경쓰지 않을 수도 있지만 위조된 경고들은 엔드 유저들을 비롯한 게으르지만 건전한 개발자들을 혼란스럽게 만든다. 일반적인 법칙이라면 출력이 간단할수록 사람들은 일이 성공했다고 생각하기 마련이다. 필자의 모니터 위에 앉아있는 웃는 얼굴의 봉제 피카추(매력적인 여성 웹 디자이너가 생일선물로 준 것이다!)를 보고 있노라면 간단한 "ok" 메시지보다 노란색의 커다란 웃는 얼굴이 더 좋을 것 같다는 생각이 들곤 한다. 아스키 아티스트들이여, 에디터를 띄우시길!

불행히도 참/거짓 테스트는 반복적이고 잘못되기 쉽다. 위의 두 테스트 사이에 세 번째 테스트를 추가하려면("참"에서 "보이지 않는 참", 그 후에 "거짓"으로 옮겨가는 것이 좋을 것 같다) if/else 블럭을 복사하고 두 번째 테스트의 번호를 새로 매겨야 한다. 이런 과정에서 작은 버그가 생겨날 수도 있다.
        print "1..2\n";
        if (1) {
                print "ok 1\n";
        } else {
                print "not ok 1\n";
        }
        if ("0 but true") {
                print "ok 2\n";
        } else {
                print "not ok 2\n";
        }
        if (0) {
                print "not ok 3\n";
        } else {
                print "ok 3\n";
        }
첫번째 줄을 고치지 않는 실수는 흔히 일어나는 것이다. 테스트 개수가 두 개일 것으로 예상하고 있는데 세 개의 테스트가 수행되었다. Test::Harness는 혼동을 일으켜 실패한 테스트의 퍼센트가 음수로 나오는 이상한 보고를 출력하게 될 것이다. 어린 피카추가 울고 있을지도 모른다. 똑똑한 프로그래머들은 결국 좋은 프로그래밍 스타일을 적용해 자신만의 ok() 함수를 만들어낼 것이다.
        print "1..3\n";
        my $testnum = 1;
        sub ok {
                my $condition = shift;
                print $condition ? "ok $testnum\n" : "not ok $testnum\n";
                $testnum++;
        }
        ok( 1 );
        ok( "0 but true" );
        ok( ! 0 );
펄 핵심 테스트 슈트의 가장 낮은 레벨에서 이런 접근방식을 사용한다. 숫자를 붙이는 작업은 거의 자동으로 할 수 있게 만들어 두는 것이 더 간단하다. 기능이 완전한 것은 아니지만 디버그는 조금 쉬워졌다.

Test::More 둘러보기

최근의 펄 배포판에는 테스트를 더 쉽게, 거의 즐길만한 수준으로 만들기 위해 존재하는 모듈들이 몇 개 들어 있고, 이 모듈들은 모두 Test::Harness와 잘 어울린다. Perl-Unit 슈트는 Junit 프레임워크를 펄에서 다시 구현했다. 비교적 새로운 모듈인 Test::More는 Test의 기능을 뛰어넘는 것들을 몇 가지 추가해 주었다. (후자에 대해서는 편견이 좀 있다는 것을 인정하지만 다른 모듈들과 더불어 이것도 좋은 선택이다)

Test::More에는 독자적인 ok() 함수가 들어 있긴 하지만 더 구체적인 함수들에 밀려 잘 사용되지는 않는다. is()는 두 표현(expression)을 비교한다. 예를 들면 더하기 함수를 테스트하려면 간단하게 다음과 같이 해주면 된다.
        is( add(2, 2), 4 );
이 함수는 문자열과 숫자를 모두 잘 다룬다. 0.36 이후의 버전에서는 0" " (빈 문자열), 그리고 undef 값들도 구분한다(했으면 좋겠다).

like()는 정규식을 스칼라 값에 적용시킨다. 이것은 치명적인 오류를 잡아내는데도 유용하다.
        $self->eat("garden salad"):

        eval { $self->write_article() };
        like( $@, qr/not enough sugar/ );
두 번째 인자(argument)는 qr// 연산자 (펄 5.005에서 처음 소개되었다)로 컴파일된 정규 표현일 수도 있고, 정규 표현을 닮은 문자열일 수도 있다. 변경도 허용된다. 만일 위의 예를 펄 5.004에서 실행시키기 위해 StudlyCaps를 사용하도록 재작성해야만 한다면 아래와 같이 할 수 있다.
        eval { $self->write_article() };
        like( $@, "/NoT eNoUgH sUgAr/i" );
실제 테스트에서 쓰기에는 지나치게 깜찍/끔찍한 방법일 테지만 위의 정규 표현 형태는 완벽하게 유효한 것이다.

Test::More로 디버깅을 편하게

이미 충분히 유용하긴 하지만 그것이 Test::More의 전부는 아니다.

Test::More는 Test::Harness와 마찬가지로 테스트에 번호 붙이는 기능을 지원하며 번호를 자동으로 매겨준다. 두 가지 경우 모두 이 기능은 큰 도움이 된다. 먼저 테스트 슈트가 예기치 않게 실패할 수도 있고(die()가 호출되거나, 세그먼트 폴트(segmentation fault)가 일어나거나, 혹은 갑자기 날벼락이 떨어졌다거나) 테스트가 어쩌다가 반복될 수도 있다(잘못된 chdir() 호출때문에, 혹은 루프 조건에 입력된 기대하지 않았던 값들, 또는 타임 워프 때문에). Test::Harness는 실제로 수행된 테스트 개수가 기대했던 개수와 맞지 않으면 경고 메시지를 내보낸다. 프로그래머는 얼마나 많은 테스트가 수행될 것인지만 말해주면 된다. Test::More를 사용하면 아래와 같이 하게 된다.
        use Test::More tests => 50;
새로운 테스트들을 작성할 때면 테스트의 개수가 몇 개나 될지 모를 수도 있다. 이 때는 no_plan 옵션을 사용한다.
        use Test::More "no_plan";
극단 프로그래밍에서는 게임처럼 접근할 것을 추천한다. 테스트를 하나 추가하고, 실행시키고, 테스트를 통과하기 위한 코드를 작성하고, 반복한다. 테스트가 끝나면 use가 들어있는 줄을 수정해 실제 테스트의 개수를 넣어준다.

Test::More 는 실패도 우아하게 처리해준다. 다음 파일이 최종테스트라고 해보자.
        use Test::More tests => 4;
        is( 1, 1 );
        is( !0, 1 );
        is( 0, 0 );
        is( undef, 1 );
Test::More 는 Test::Harness 를 이용하지 않고 독자적으로 수행되어 아래의 결과를 보여준다.
        1..4
        ok 1
        ok 2
        ok 3
        not ok 4
        #     Failed test (numbers.t at line 6)
        #          got: undef
        #     expected: "1"
        # Looks like you failed 1 tests of 1.
이 에러 메시지는 테스트가 들어있는 파일의 이름, 실패한 테스트의 번호, 그리고 실패한 테스트의 행 번호, 기대했던 값과 실제로 받은 값을 모두 제공한다. 디버깅이 더 쉬워진 것이다. 에러를 찾아내기 위해 테스트 개수를 세어보는 작업을 한 번만 하면 된다. 아마 이 방식이 마음에 들 것이다.

Test::Harness 는 또 옵션으로 테스트 메시지에 부가되는 테스트 주석을 지원한다. 다시 말해 간단한(raw) 테스트에서 아래와 같이 쓸 수 있다는 것이다.
        print "ok 1 # the most basic test possible\n";
거의 모든 Test::More의 함수들이 이것을 선택적인 파라미터로 받아들인다.
        ok( 1, "the number 1 should evaluate to true" );
        is( 2 + "2", 4, "numeric strings should numerify in addition" );
        like( "abc", qr/z*/, "* quantifier should match zero elements" );
이러한 이름들은 사회적 관례상 요구되는 것일 뿐이다. 작은 테스트 명령이라고 생각해 보라. 만일 테스트가 잘못 되었거나 또는 다시는 재발하지 않아야 하는 이미 고쳐진 버그를 또 보여준다면, 이렇게 설명을 해주는 이름을 붙여 놓았을 경우 무엇을 테스트하고 있는 것인지 쉽게 알 수 있을 것이다. Test::Harness는 이렇게 넘어오는 값을 조용히 무시해 버리지만 수동으로 실행시켰을 때는 볼 수 있다.
        ok 1 - the number 1 should evaluate to true
        ok 2 - numeric strings should numerify in addition
        ok 3 - * quantifier should match zero elements
수동 테스트는 개선된 버그리포트를 만들어준다. 불편을 감수하고 싶다면 이와 같은 편리한 도구를 무시해도 상관은 없다.

Test::More의 기타 기능들

앞에서 설명한 기능들이 충분하지 않다고 느끼는가? Test::More에는 더 많은 기능들이 들어있다. 그 중 하나는 테스트를 생략할 수 있게 해주는 것이다. 가끔은 어떤 기준을 만족하면 특정 기능을 테스트해볼 필요가 없을 경우가 있다. 앞서 설명한 qr// 연산자를 생각해 보자. 펄 5.004와 하위호환성을 유지해야 하는 모듈일 경우 생략할 수 있는 테스트가 들어있는 테스트 슈트는 아래와 같이 처리할 수 있다.
        SKIP: {
                skip( "qr// not supported in this version", 2 ) unless $] >= 5.005;
                my $foo = qr/i have a cat/;
                ok( "i have a caterpillar" =~ $foo,
                        "compiled regex should match similar string" );
                ok( "i have a cold" !~ $foo,
                        "compiled regex should not match dissimilar string" );
        }
요약 하자면, 우선 생략할 수 있는 테스트들은 레이블로 명명된 블럭에 들어있다. 이때 레이블의 이름은 SKIP 이여야 한다. (걱정하지 마시라. 같은 이름의 블록이 하나의 파일에 여러 개 있어도 괜찮다.) 그 다음 테스트를 생략할 것인지 아닌지를 결정하는 조건문이 있어야 한다. 위의 예에서는 현재 펄의 버전을 찾기 위해 perlvar의 특수변수를 체크한다. skip() 함수는 펄 구버전에서 실행되었을 때만 호출될 것이다.

skip() 함수는 독특한 파라미터 순서때문에 헷갈리기 쉽다. 첫번째 인수는 테스트가 생략되었을 때 보이는 이름이다. 두 번째 인자는 생략할 테스트의 수다. 혼동의 여지가 있기 때문에 이것은 블럭 안에 들어있는 테스트의 수와 일치해야 한다. 펄 5.004에서 실행시키면 위의 테스트는 아래의 결과를 보여준다.
        ok 1 # skip qr// not supported in this version
        ok 2 # skip qr// not supported in this version
ok 라는 메시지가 나왔지만 Test::Harness는 이 테스트가 생략되었다는 것을 알고 제대로 통과된 것이 아니라 생략되었다고 보고할 것이다. 이 기능은 플랫폼이나 버전 차이로 인해 절대로 실행되어서는 안될 테스트들에 대해서만 사용하는 것이 좋다. 이와 같은 사항을 확실히 모르는 테스트를 할 경우 todo()를 사용해라.

이 모두가 Test::Builder::ok()를 기초로 해서 만들어지긴 했지만 다른 함수들을 사용하면 같은 일을 더 빨리 할 수 있는 경우가 많다. use_ok()require_ok를 사용하면 이름이 주어진 파일을 로드 하거나 필요에 따라 임포트 하고 성공했는지 실패했는지를 보고한다. 이 기능은 모듈을 찾아서 컴파일 할 수 있는지를 검사해 주며 테스트 슈트에서 가장 먼저 사용되는 테스트인 경우가 많다. can_ok() 함수는 클래스나 객체의 메소드를 찾을 수 있는지 알아본다. isa_ok()는 상속을 체크한다.
        use_ok( "My::Module" );
        require_ok( "My::Module::Sequel" );
        my $foo = My::Module->new();
        can_ok( $foo->boo() );
        isa_ok( $foo, "My::Module" );
그 결과는 자신들의 이름으로 보고한다.
        ok 1 - use My::Module;
        ok 2 - require My::Module::Sequel;
        ok 3 - My::Myodule->can(boo)
        ok 4 - object->isa("My::Module")
다른 함수들과 기능들은 Test::More 문서에 설명되어 있다. 또한 Test::Tutorial 맨페이지에서는 비슷한 내용을 다른 분위기로 설명하고 있다.

마지막으로, 좋은 프로그래밍 습관을 잊지 말기 바란다. 테스트 함수들은 단지 표준적인 서브루틴일 뿐이다. 테스트들은 그저 펄 코드일 뿐이다. 일을 쉽게 할 수 있다면 루프, 변수, helper subs, map()를 비롯하여 뭐든지 사용해도 된다. 예를 들어 기본적으로 상속된 인터페이스를 테스트하는 작업은 아래와 같이 쉽게 할 수도 있다.
        # see if IceCreamBar inherits these methods from Popsicle
        my $icb = IceCreamBar->new();
        foreach my $method (qw( fall_off_stick freeze_tongue drip_on_carpet )) {
                can_ok( $icb, $method, "IceCreamBar should be able to $method()" );
        }
여러 개의 can_ok() 테스트를 각각 하는 것보다 훨씬 낫다. 테스트 이름에 메소드 이름을 끼워넣는 것도 편하다.

결론

불행하게도 테스트는 종종, 특히 자유 소프트웨어 프로젝트에서는 무시되곤 한다. 많이 자고, 채소를 먹고, 정기적으로 운동을 하는 것을 테스트로 간주해 보아라. 처음에는 경련이 날 수도 있지만 계속해서 한다면 모든 것들이 엄청나게 향상되기 시작할 것이다. (만일 아주 커다란 시스템, 예를 들어 펄 같은 시스템에 테스트가 있는데 거기에다가 독자가 테스트들을 만들어야 한다면 다른 결과가 나올 수도 있다)

펄의 목표 중 하나는 사용자들의 삶을 편리하게 만들어 주는 것이다. 펄 5.8에는 Test::More와 그 친형제인 Test::Simple 맨페이지Test::Builder 맨페이지가 포함되어 있다. 이들은 테스트 작성을 덜 번거롭게, 반대로 즐겁게 만들어 주기 위해 존재한다. 한 번 사용하는 것을 진지하게 고려해 보라.

테스트를 작성과 관리가 쉬워지면 더 많은 사람들이 테스트를 하게 될 것이다. 더 많은, 더 좋은 테스트들은 소프트웨어의 포팅가능성, 유지보수용이성, 그리고 신뢰성을 높여준다. 독자가 지금은 테스트하는 것을 멀리 있는 것으로 생각할지도 모른다. Test::More나 다른 프레임워크를 한 번 사용해 보라. 그러면 이 모듈들이 친근하게 보일 것이다. 테스트는 정말로 유익한 것이다!
TAG :
댓글 입력
자료실

최근 본 책0