 |
|
난이도 : 중급 Brandon Craig Rhodes, Independent Consultant, Rhodes Mill Studios, Inc.
원문 게재일 : 2009 년 6 월 02 일 번역 게재일 : 2009 년 9 월 01 일 최근에 뛰어난 기능을 제공하는 Python 테스트 프레임워크가 등장하기 시작하면서
한결 간결하고 일관된 형태의 Python 테스트를 작성할 수 있게 되었으며 더 나아가 결과를 보고하는
기능도 크게 향상되었습니다. 이 기사에서는 우수한 테스트 프레임워크의 강력한 자동 검색 기능을
통해 애플리케이션 테스트를 검색하는 방법과 관리 작업에 사용되는 불안정한 중앙 테스트 목록을
대체하는 방법에 대해 설명합니다.
Python 프로그래밍 커뮤니티는 유닛 테스트 및 기능 테스트를 지지하는 것으로
잘 알려져 있다. 이러한 테스트는 구성 요소와 애플리케이션을 처음부터 올바르게 작성하는
데 도움이 되기도 하지만 복잡한 문제를 해결하거나 기능을 개선하기 위해 수개월에서 수년의
시간이 걸리기도 한다.
이 기사는 최신 Python 테스트 프레임워크에 대해 설명하는 세
편의 기사로 구성된 시리즈의 두 번째 기사이다. 이 시리즈의 첫
번째 기사에서는 zope.testing, py.test
및 nose를 소개한 후 이러한 도구로 인해 Python 프로젝트에서 테스트를
작성하고 관리하는 방법이 어떻게 변경되었는지에 대해 설명한다. 이 두 번째 기사에서는 프레임워크를
호출하는 방법, 프로젝트를 조사하여 테스트를 검색하는 방법 및 테스트를 선택하고 실행하는 방법이
프레임워크별로 어떤 차이가 있는지 살펴본다. 마지막으로 세 번째 기사에서는 테스트 지원 기술의 성능을
강화하기 위해 개발된 모든 보고 기능에 대해 설명한다.
Python 테스트의 암흑기
한때는 Python 프로젝트 테스트를 아주 일시적이고 개인적인 작업으로 간주했던
시절도 있었다. 초기에서는 개발자가 일괄 처리 테스트를 매번 별도의 Python 스크립트로
작성했다. 그 이후에는 모든 테스트를 가져와서 실행하는 test_all.py,
tests.py 등의 이름을 가진 스크립트를 작성했다. 하지만 한
개발자가 프로세스를 자동화한 것이기에 필연적으로 이 스크립트에는 해당 개발자의 특성이
반영될 수 밖에 없었다. 즉, 프로젝트에 참여한 모든 개발자가 테스트 스크립트의 위치와
호출 방법을 알고 있어야만 했다. 십여 개의 각기 다른 프로젝트에 참여하고 있는 개발자의
경우 십여 개의 각기 다른 테스트 명령을 기억하고 다녔을 것이다.
프로젝트마다 이름이 다를 수 있는 test_all.py 스크립트에서
다른 모든 테스트를 수동으로 가져온다는 점 때문에 위험이 발생한다. 이 중앙 테스트 목록이 최신 상태로
유지되지 못했다면, 보통은 개발자가 수동으로 새로운 테스트 스위트를 추가 실행했으나 중앙 스크립트에
추가하는 것을 잊어버려 완전한 테스트 파일이 Python 패키지가 프로덕션 단계로 들어가기 전에 수행되는
최종 테스트 스위트 실행에서 누락되었기 때문이다.
이 어지러운 테스트 방법의 마지막 결점은 별도의 명령으로 실행하기 위해 필요한 상용구 코드가
모든 테스트 파일에 있어야 한다는 것이다. 여러 Python 설명서뿐만 아니라 오늘날의 일부 Python 프로젝트에서도
다음과 같은 테스트 예제를 많이 볼 수 있다.
# test_old.py - The old way of doing things
import unittest
class TruthTest(unittest.TestCase):
def testTrue(self):
assert True == 1
def testFalse(self):
assert False == 0
if __name__ == '__main__':
unittest.main() |
이 시리즈의 첫
번째 기사에서 이미 오늘날 TestCase 클래스 기반 테스트가 필요하지
않는 이유에 대해서 다루었다. 하지만 여기에서는 코드의 마지막 두 행을 살펴보자. 과연 이 두 행은
어떤 작업을 수행하는 것일까? 이 두 행은 명령행에서 test_old.py 스크립트가
독립적으로 실행되는 시기를 감지하다가 스크립트가 독립적으로 실행되면 모듈에서 테스트를 검색하여 실행하는
unittest 편의 함수를 실행한다. 바로 이 점 때문에 이 테스트 파일이 프로젝트
테스트 스크립트와 별도로 실행되는 것이다.
이와 같이 동일한 코드를 수십 또는 수백 개의 테스트 모듈에 복사하는 방법에는
명확히 드러나는 단점이 많이 있다. 명확하게 드러나지 않는 단점 중 하나는 표준화가 되어
있지 않다는 점이다. test_main() 함수가 부실하여 일부 특정
모듈의 테스트가 검색되지 않을 경우 해당 모듈은 다른 테스트의 작동 방식과 일치하지 않는
추가 동작을 통해 확장된다. 결국 각 모듈에서 테스트 클래스에 이름을 지정하는 방법, 테스트
클래스가 작동하는 방법 및 실행되는 방법 등에 대한 규칙이 약간씩 달라질 수 있다.
새로운 시대의 Python 테스트
주요 Python 테스트 프레임워크의 등장으로 인해 앞에서 언급한 모든 문제가 해결되었다.
그리고 이러한 문제는 각 프레임워크에서 거의 비슷한 방법을 통해 해결되었다.
우선, 세 가지 테스트 프레임워크 모두 운영 체제 명령행에서 테스트를 실행하는 것과
관련하여 표준화된 방법을 제공하고 있기 때문에 모든 Python 프로젝트에서 코드 내에 전역 테스트
스크립트를 가지고 있을 필요가 없어졌다.
zope.testing 패키지의 경우 모두가 예상하듯이 가장 독특한 방식으로
테스트를 실행한다. Zope 개발자는 buildout을 사용하여 프로젝트를 설정하기
때문에 buildout.cfg 파일의 zc.recipe.testrunner
레서피를 통해 테스트 스크립트를 설치한다. 그럼에도 불구하고 다양한 프로젝트에서 매우 일관된 결과를
볼 수 있다. 즉, 필자가 우연히 보았던 모든 Zope 프로젝트의 개발 buildout에서 프로젝트의 테스트를
호출할 것으로 여겨지는 ./bin/test 스크립트가 작성되었다.
py.test 및 nose 프로젝트의 경우에는
더욱 흥미로운 방법이 사용된다. 이들 프로젝트에서 제공되는 명령행 도구를 사용하면 각 프로젝트에서
고유한 테스트 명령을 아예 사용하지 않아도 된다.
# Run "py.test" on the project
# in the current directory...
$ py.test
# Run "nose" on the project
# in the current directory...
$ nosetests |
게다가 py.test 및 nosetests 도구에는
공통으로 공유하는 몇 가지 명령행 옵션도 있다. 예를 들어, -v 옵션을 사용하면
실행되는 각 테스트의 이름이 표시된다. 이러한 두 도구에 익숙해지고 나면 Python 프로그래머가 공개적으로
사용할 수 있는 Python 패키지의 테스트를 실행할 수 있는 날이 곧 올 것이다.
마지막으로 실현 가능한 표준화 방법이 하나 더 있다. 오늘날 대부분 Python 프로젝트의 소스 코드에는
다음과 같은 명령을 지원하는 최상위 수준 setup.py 파일이 있다.
# Common commands supported by setup.py files
$ python setup.py build
$ python setup.py install |
오늘날 많은 Python 프로젝트에서는 프로젝트의 모든 테스트를 실행하는 test
명령과 같이 표준 Python을 통해 사용할 수 있는 명령이 아닌 추가 setup.py 명령도
지원하기 위해 setuptools 패키지를 사용하고 있다.
# If a project's setup.py uses "setuptools"
# then it will provide a "test" command too
$ python setup.py test |
\
이 방법은 표준화 작업의 일부에 불과하다. 모든 프로젝트에서 setup.py test가
일관되게 지원되었다면 모든 Python 패키지의 테스트를 실행할 수 있는 단일화된 형태의 인터페이스가 개발자에게
제공되었을 것이다. 다행히도 nose에서는 nosetests 명령에
사용되는 것과 동일한 테스트 실행 루틴을 호출하는 진입점을 제공하는 방식으로 setup.py가
지원된다.
# A setup.py file that uses "nose" for testing
from setuptools import setup
setup(
# ...
# package metadata
# ...
setup_requires = ['nose'],
test_suite = 'nose.collector',
) |
물론 대부분의 개발자는 nosetests의 명령행 옵션이 유용하기
때문에 프로젝트에서 setup.py 진입점이 제공된다고 하더라도 nosetests를
계속 사용할 것이다. 하지만 버그를 추적하거나 새 기능을 추가하기 전에 패키지가 자신의 플랫폼에서
플랫폼에서 작동하는지 여부만을 확인하려는 초보 개발자라면 test_suite
진입점을 편리하게 사용할 수 있을 것이다.
자동 Python 모듈 검색
zope.testing, py.test
및 nose의 주요 기능은 프로젝트의 소스 코드 트리를 검색하여
모든 테스트를 찾아내는 것이며, 이 경우 테스트를 중앙 집중적으로 나열하지 않아도 된다. 하지만
이러한 도구에서 테스트를 검색할 때 사용되는 기준이 도구마다 조금씩 다르므로 프레임워크를
선택하기 전에 각 도구의 기준을 검토해 보아야 한다.
테스트 프레임워크에서 가장 먼저 수행하는 작업은 테스트가 포함되어 있는 파일을
검색할 디렉토리를 선택하는 것이다. 세 프레임워크 모두 전체 프로젝트의 기본 디렉토리부터
검색을 시작한다. 예를 들어, example이라는 패키지를 테스트하는
경우에는 example이 들어 있는 상위 디렉토리에 있는 테스트부터
검색하기 시작한다. 하지만 이들 세 프레임워크는 조금씩 다른 기준을 가지고 검색할 디렉토리를
선택한다.
zope.testing 도구는 Python 패키지인 모든
디렉토리 즉, __init__.py 파일(Python에서 import
명령문을 사용하여 가져올 수 있는 디렉토리라는 의미)이 들어 있는 모든 디렉토리를
재귀적으로 검색한다. 이는 곧 패키지가 아닌 디렉토리에 있는 데이터와 코드는 검사되지
않는다는 의미이다. 이를 역으로 생각하면 이론적으로 프로그래머가 원할 경우 import
명령문을 사용하여 다른 개발자가 작성한 모든 테스트를 검색할 수 있다는 의미이기도
하다. 다른 사람이 자신의 테스트를 보는 것을 불쾌하게 생각하게 하는 프로그래머의
경우 패키지의 일반 사용자가 볼 수 없는 위치에 테스트를 배치할 수 있다.
py.test 명령은 디렉토리가 Python 패키지인지
여부와 상관 없이 모든 디렉토리와 서브디렉토리를 검색한다. 인접한 두 디렉토리에 같은
이름을 가진 테스트가 있으면 버그이다. 예를 들어, 인접한 dir1/test.py
및 dir2/test.py 파일에 test_example
테스트가 모두 있을 경우 py.test는 첫 번째 테스트를 두 번
실행하고 두 번째 테스트를 완전히 무시한다. py.test에 대한
테스트를 작성한 후 패키지가 아닌 디렉토리에 있는 테스트를 숨길 경우에는 테스트에 고유한
이름을 지정해야 한다.
nose 테스트 실행 프로그램은 다른 두 도구의
방법을 결합한 형태의 방법을 구현한다. 즉, 모든 Python 패키지를 검색하기는 하지만 이름에
test가 포함된 디렉토리만을 검색한다. 따라서 nose가
디렉토리 내의 테스트를 검색하지 않도록 디렉토리를 "보호"하려면 디렉토리 이름에 test가
포함되지 않도록 주의하기만 하면 된다. py.test와는 달리 nose는
인접 디렉토리에 같은 이름을 가진 테스트가 있더라도 올바르게 작동한다. (하지만 -v
옵션을 사용하여 테스트 결과를 볼 때 잘 구별할 수 있도록 테스트 이름을 고유하게 유지하는 것이 좋다.)
검색할 디렉토리를 선택한 후에는 세 테스트 도구의 동작이 매우 비슷하다. 즉, 이들 도구는
모두 일부 특정 패턴과 일치하는 Python 모듈(즉, .py로 끝나는 파일)을
검색한다. zope.testing 도구는 기본적으로 "tests"이라는
정규 표현식을 사용하므로 tests.py라는 이름의 파일만을 찾고 나머지
파일을 모두 무시한다. 다음과 같이 명령행 옵션이나 buildout.cfg를 사용하여
대체 정규 표현식을 지정할 수 있다.
# Snippet of a buildout.cfg file that searches for tests
# in any Python module starting with "test" or "ftest".
[test]
recipe = zc.recipe.testrunner
eggs = my_package
defaults = ['--tests-pattern', 'f?test'] |
좀 더 엄격한 py.test 도구는 항상 `test_로
시작하거나 _test로 끝나는 이름을 가진 Python 모듈을 검색한다. 좀 더 유연한
nosetests 명령은 test 또는 Test라는
단어로 시작하거나 단어 경계 뒤에 해당 단어가 있는 모듈을 선택하는 정규 표현식("((?:^|[\b_\.-])[Tt]est)")을
사용한다. 명령행에서 -m 옵션을 사용하거나 프로젝트의 .noserc
파일에서 이 옵션을 설정하여 다른 정규 표현식을 지정할 수 있다.
그렇다면 어떤 방법이 가장 효과적일까? 항상 유연성을 선호하는 개발자들이 일부
있기는 하지만 zope.testing 도구가 tests.py
이외의 파일 이름을 사용하는 모듈까지도 검색할 수 있도록 확장된다면 모를까 그렇지 않다면
검색 작업이 매우 복잡해질 수 있다. 이러한 경우 필자는 실제로 py.test를
자주 사용한다. py.test를 사용하는 모든 프로젝트에서는 다른
프로그래머가 테스트를 쉽게 읽고 관리할 수 있도록 공통 규칙에 따라 테스트 이름을 지정해야
한다. 사용하는 프레임워크가 서로 다를 경우에는 테스트 파일을 읽거나 작성하는 데 두 단계의
작업이 필요하다. 즉, 먼저 특정 프로젝트에 사용되는 정규 표현식을 찾아야 한다. 그런 다음
정규 표현식이 있어야만 해당 코드에 대한 검색을 시작할 수 있다. 동시에 여러 프로젝트를 수행할
경우에는 서로 다른 여러 테스트 파일 이름 규칙을 동시에 관리해야 한다.
테스트에 docfile 및 doctest 포함하기
눈에 잘 띄는 세 개의 갈매기 표시로 이루어진 Python 프롬프트 >>>는
매우 명확한 신호로 인식되고 있다. 긴 문서에서 이 기호는 Python 프롬프트에서 발생해야 하는 작업을
보여 준다. 이 기호는 이 시리즈의 첫 번째 기사에서 보았던 것처럼 문서 기능을 수행하는 독립된 텍스트
파일에 사용된다.
Doctest for truth and falsehood
-------------------------------
The truth values in Python, named "True" and "False",
are equivalent to the Boolean numbers one and zero.
>>> True == 1
True
>>> False == 0
True |
이러한 설명은 다음과 같이 소스 코드에 있는 모듈, 클래스 또는 함수의 docstring에서도 사용될 수 있다.
def count_vowels(s):
"""Count the number of vowels in a string.
>>> count_vowels('aardvark')
3
>>> count_vowels('THX')
0
"""
return len( c for c in s if c in 'aeiou') |
첫 번째 예제와 같이 이러한 테스트가 텍스트 파일에서 발생할 경우 이 파일을 docfile이라고
한다. 그리고 두 번째 예제와 같이 Python 소스 코드 내의 docstring에서 발생할 경우에는 이러한 테스트를
doctest라고 한다.
docfile과 doctest는 둘 다 자체적으로 테스트 기능을 수행하는(그리고 기한이 경과되어 유효하지
않게 되었을 때 신호를 보내는) 문서를 작성하는 데 자주 사용되는 방법이기 때문에 py.test
및 nose에서 직접 지원된다. (zope.testing 사용자는
표준 doctest 모듈의 DocTestSuite 클래스를 사용하여
각 파일에 대한 Python 테스트 케이스를 수동으로 작성해야 한다.)
테스트 모듈을 검색하는 규칙과 관련하여 py.test 프레임워크는 특정
프로젝트 내의 유연성 대신 프로젝트 간의 표준화를 선택함으로써 구성할 수 없는 doctest를 지원할 수 있도록
절차를 수정했다.
- 이 프레임워크의
-p doctest 플러그인을
활성화하면 모든 Python 모듈(이름에 test가 포함되지 않은 모듈 포함)의 문서
문자열과 test_로 시작하고 .txt 확장자로 끝나는
이름을 가진 텍스트 파일에서 doctest가 검색된다.
- 이 프레임워크의
-p restdoc 플러그인을
활성화하면 .txt 파일의 모든 doctest가 테스트될 뿐만 아니라 py.test가
프로젝트의 모든 .txt 파일이 유효한 재구조화된 텍스트 파일인지 여부를 검사하여
구문 분석 오류가 있는 파일이 있을 경우 메시지를 표시한다. 또한 이 플러그인의 추가 명령행 옵션을 사용하여
문서에 지정된 URL을 검사한 후 각 .txt 문서 파일의 HTML 버전을 미리 생성할 수도
있다.
nose에서도 매우 비슷한 기능 세트가 지원되기는 하지만 이
경우에는 우리가 예상할 수 있듯이 유연성이 좀 더 높다.
--doctest-tests는 간섭이 가장 없는 옵션으로 nose가
이미 검사하고 있는 테스트 모듈의 docstring에 있는 doctest를 검사한다.
- 좀 더 적극적인
--with-doctest 옵션을 선택하면
nose가 테스트가 아닌 일반적인 코드를 가지고 있는 모든 일반
모듈에서 docstring에 있는 모든 doctest를 찾아서 실행한다.
- 마지막으로
--doctest-extension 옵션의 경우에는
사용자가 파일 이름 확장자를 지정한다. (대부분의 개발자가 .txt,
.rst 또는 .doctest를 선택한다). 이
경우 nose는 프로젝트에서 지정된 확장자를 가진 모든 텍스트 파일을
읽고서 검색된 모든 doctest를 실행하고 확인한다.
py.test와 nose의 기능 집합이
매우 비슷하기는 하지만 그래도 필자는 nose를 주로 사용한다. 필자는
모든 재구조화된 파일에 표준이 아닌 .rst 확장자를 사용하기를 좋아한다.
이렇게 하면 텍스트 편집기에서 해당 파일을 인식하도록 쉽게 지정할 수 있고 특별한 구문 강조 표시를
지정할 수 있기 때문이다.
nose 프레임워크 및 실행 모듈
nose 프레임워크의 경우 한 가지 주의할 사항이 있다. 이 프레임워크에서는
기본적으로 실행 모듈로 표시된 Python 모듈을 사용하지 않아야 한다. (Linux®에서 chmod +x
등과 같은 명령을 사용하여 파일을 실행 명령으로 표시할 수 있다.) 명령행에서 직접 실행되도록 설계된 모듈의
경우 일부 작업에 의해 이러한 파일이 import에 안전하지 않은 파일이 될 수 있기 때문에
nose 프레임워크에서는 이러한 파일이 무시된다.
하지만 모듈이 실행 중인지 또는 단순히 모듈을 가져오기만 하는 것인지를 검사하는 if
명령문을 사용하여 수행되는 실제 작업을 보호하면 import에 안전한 명령을 만들 수 있다.
#!/usr/bin/env python
# Sample Python command
if __name__ == '__main__':
print "This has been run from the command line!" |
모든 명령에서 이 조치를 취하게 되면 명령이 import에 안전하다는 것을 알고 있으므로
nose에서 --exe 명령행 옵션을 사용하여
그러한 모듈을 검사할 수 있다.
이 경우 필자는 실제로 py.test를 주로 사용한다. 즉, Python
모듈이 실행 모듈인지 여부를 무시하고서 nose의 규칙보다 단순하고
좋은 사례가 적용된 규칙이 사용된다(예: if 명령문으로 명령 논리
보호). 하지만 코드 품질을 확신할 수 없는 실행 모듈이 많이 포함된 레거시 애플리케이션에 테스트
프레임워크를 실제로 사용할 경우에는 nose를 사용하는 것이 더 안전하다.
결론
이 기사에서는 이들 세 Python 테스트 프레임워크에서 코드를 검사하여 테스트가
있을 것으로 생각되는 모듈을 선택하는 방법에 대해 자세히 살펴보았다. 단일화된 규칙을
기반으로 하는 자동 검색을 제공하면 세 가지 주요 테스트 프레임워크 중 어느 것을 사용하더라도
시스템에서 감지 및 조사할 수 있는 일관된 테스트를 효율적으로 작성할 수 있다. 그렇다면
웹 프레임워크에서 수행되는 다음 작업은 무엇일까? 모듈 내부에서 무엇을 찾아야 할까? 이
시리즈의
세 번째 기사에서 이 질문에 대한 답을 찾을 수 있다.
참고자료 교육
토론
필자소개  | |  | Brandon Craig Rhodes는 Python Magazine의 편집장으로 활동하고 있으며 10년 이상의 Python 언어 경력을
가지고 있는 독립 웹 애플리케이션 컨설턴트이다. 업계 수준의 계산 천문학 루틴에 대한 오브젝트 지향적 인터페이스를
제공하는 PyEphem 확장 모듈을 직접 개발하여 9년 이상 관리해 오고 있으며 여러 대륙의 천문학자들이 이 확장 모듈을
사용하고 있다. Brandon은 Python Atlanta 사용자 그룹의 조정자이기도 하다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|