Extremely Agile/TDD2008.05.21 17:20

찾아보니까 세상에는 굉장히 많은 단위 테스트 툴들이 있더군요. jUnit은 그 중 가장 유명하다 할만 합니다. 그런데 이런 단위 테스트 툴들이 없었을때에는 도대체 어떻게 단위 테스트를 했을까요? -_-

C를 위한 단위 테스트 프레임워크로 Check라는 것이 있습니다. (세상에 존재하는 모든 단위 테스트 프레임워크의 목록을 보고 싶다면 www.xprogramming.com의 관련 페이지에 가보시는게 좋겠군요.  저도 그 중 몇 가지 단위 테스트 프레임워크는 써 봤구요.) 배워볼까 싶어 잠깐 훑어봤는데, 이런 생각이 들더군요.

단위 테스트를 하기 위해서 꼭 특정한 테스트 프레임워크의 사용법을 배워야 한다면, 좀 귀찮지 않을까요? 제가 배우고 있는 언어만도 Java, C++, C, Ruby, Erlang, Scheme 등등 많은데, 그럼 대체 몇 가지의 테스트 프레임워크를 설치해야 하나요?

요즘은 Eclipse 덕에 이런 저런 프로그래밍 언어들을 동일한 환경에서 사용하기가 편해졌다고는 합니다만, 가끔은 환경 설정하고 프로그램 설치하고 하는 것도 귀찮을 때가 있거든요. 특히 시간이 부족해서 후달릴때는 더더욱.

그래서 이번에는 그냥 assert만 사용해서 단위 테스트를 해 봤습니다. 뭐 CppUnit 써서 테스트 할 때와 엇비슷한 코드가 나오더군요 ㅎㅎ (제가 훌륭한 프로그래머가 아니라서 그럴지도 -_-)

개략적인 코드 얼개만 잠시 보시면...

....    // 열심히 짠 C 모듈 코드.

#ifdef _TEST

/*
 * test DRIVER program. Just to test this module.
 */

HashTableBlock hash_table;
HashTableEntryBlock hash_table_entries;

int main() {
    ...
    HashTableEntry* e1 = ...;
    assert( e1 != 0 );
    ...
    HashTableEntry* e3 = ...;
    assert( e3->src_addr == 0x7f000001 );
    assert( e3->src_port == 3086 );
    ...

    int i = 0;
    for( i = 0; i < 1050000; ++i ) {
        HashTableEntry* ee = alloc_hash_entry_block(&hash_table_entries);
        assert( ee != 0 );
    }
    return EXIT_SUCCESS;
}

#endif

#ifdef와 #endif를 사용했기 때문에, test driver 코드는 _TEST가 명시된 경우에만 생성됩니다.

원래는 Check를 써서 테스트를 할까 했는데, 직접 assert를 사용해서 테스트를 해 봐도 뭐 그렇게 테스트가 어려워진다거나 하지는 않는 것 같아요. 결국 취향 문제인데, 테스트 결과를 일목요연하게 정리해서 보여준다거나 하는 기능이 정말로 아쉬운 사람이라면 assert 대신 다른 테스트 프레임워크를 사용하는 것이 낫겠지만, 그런 리포트에 연연하지 않는 사람이라면 이렇게 해도 별 상관 없을 것 같아요.

어차피 단 1개의 테스트만 실패했더라도, 실패한 것은 실패한 것이니까요.

[여기까지 작성한 다음에 출장을 다녀옴 -_-]

위와 같이 assert를 사용한 단위 테스트 방안을, 커널 모듈을 테스트하는 데 응용해 봤습니다. 원래 커널 모듈을 컴파일하기 위해 제가 사용했던 Makefile은 대략 다음과 같이 생겼습니다.

obj-m := captureapp.o

clean:
    \rm -f *.ko *.o *.mod.c


좀 심하게 단순하죠 -_-; 최상위 단계가 clean인 덕에, make를 때리나 make clean을 때리나 효과는 똑같습니다. 아무튼, 이 Makefile을 다음과 같이 바꾸었습니다. (2008년 5월 26일 수정됨)

obj-m := captureapp.o

UNITTEST_SRCS = hash_table_test.c
UNITTEST_EXES = ${UNITTEST_SRCS:.c=}

unittest : ${UNITTEST_EXES}

${UNITTEST_EXES} : ${UNITTEST_SRCS}
    gcc -D_TEST $< -o $@

clean :
    \rm -f *.ko *.o *.mod.c ${UNITTEST_EXES}


따라서 make unittest를 때리면, 단위 테스트를 실행하는 실행파일들이 테스트 타겟별로 만들어지게 됩니다. 위의 경우에는 현재 테스트 타겟이 hash_table 밖엔 없어서 hash_table_test라는 실행파일만 만들어지게 되죠.

hash_table.h에는 테스트 타겟인 hash table관련 코드들이 들어가있게 되구요. hash_table_test.c에는 그 코드들에 대한 단위 테스트 루틴들이 들어가게 됩니다. 우선 hash_table.h를 보시면, 코드 윗부분에 다음과 같은 매크로 디렉티브들이 들어가 있습니다.

#ifdef _TEST
#include <stdint.h>
#include <assert.h>
#include <string.h>
#define u32 uint32_t
#define u16 uint16_t
#define u8 uint8_t
#else
#include <linux/module.h>
#define assert(x)
#endif

hash_table.h의 코드가 커널 모듈에 들어갈 코드이긴 하지만, 그 코드가 커널 루틴을 심하게 건드리는 코드가 아닌 단순히 커널 모듈 안에서 사용될 라이브러리 코드라면, 사용자 공간에서 테스트를 하는 것이 낫거든요. 그래서, 위와 같은 매크로 디렉티브들을 사용하여 커널 코드에서 사용되는 타입과 사용자 공간에서 사용되는 타입들을 일치시킵니다. assert는 커널 모듈안에서는 사용할 수 없으니까 제거해주는 부분도 넣었구요.

이제 hash_table_test.c를 보시면 되겠군요. 만일의 경우를 대비해 hash_table_test.c의 코드 대부분은 #ifdef _TEST ... #endif 사이에 정의됩니다. 앞서 보셨던 코드와 별반 다를 것은 없습니다.

#include <stdlib.h>
#include "hash_table.h"

#ifdef _TEST

/*
 * test DRIVER program. Just to test this module.
 */

HashTableBlock hash_table;
HashTableEntryBlock hash_table_entries;

int main() {
    ...
    HashTableEntry* e1 = ...;
    assert( e1 != 0 );
    ...
    HashTableEntry* e3 = ...;
    assert( e3->src_addr == 0x7f000001 );
    assert( e3->src_port == 3086 );
    ...

    int i = 0;
    for( i = 0; i < 1050000; ++i ) {
        HashTableEntry* ee = alloc_hash_entry_block(&hash_table_entries);
        assert( ee != 0 );
    }
    return EXIT_SUCCESS;
}

#endif


적고보니 단위 테스트를 '프레임워크 없이'도 좀 체계적으로 진행하려면 준비해야 할 것이 꽤나 많은데요. 아마 단위 테스트 프레임워크를 사용하는 이유 중 하나는 이런 체계를 어떻게 만들어야 할 지 감이 잘 오지 않기 때문이겠죠. 만들고 나면 사실 별거 아닐 수도 있습니다만...

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^