 |
|
난이도 : 중급 Brandon Craig Rhodes, Software Engineer, Rhodes Mill Studios, Inc.
원문 게재일 : 2009 년 6 월 23 일 번역 게재일 : 2009 년 9 월 29 일 최근에 뛰어난 기능을 제공하는 Python 테스트 프레임워크가 등장하기 시작하면서
한결 간결하고 일관된 형태의 Python 테스트를 작성할 수 있게 되었으며 더 나아가 결과를 보고하는
기능도 크게 향상되었습니다. 이 기사에서는 가장 유명한 3가지 테스트 프레임워크에서 테스트를
식별 및 수집하는 방법과 공통 설정 및 해체 코드를 공유하는 전체 테스트 계층의 작성을 위해
제공되는 지원에 대해 설명합니다.
이 세 편의
기사로 구성된 시리즈의 첫 번째 기사에서는
zope.testing, py.test 및 nose와 같은
표준 테스트 프레임워크의 등장으로 획기적으로 개선된 Python 테스트에 대해 살펴보았다. 이러한 테스트 프레임워크는 단순한
테스트 관용구를 지원하며 기존에는 테스트를 실행하기 위해 프로젝트에서 작성하고 관리해야 했던 임시 코드를 대체할 수
있다. 두 번째 기사에서는 이러한 자동화된
솔루션이 Python 패키지를 검색하여 테스트가 포함된 모듈을 식별하는 방법에 대해 설명했다.
이 기사에서는 한 단계 더 나아가서 테스트 모듈을 조사하여 내부에 있는 테스트를 검색할 때
프레임워크에서 수행되는 작업을 살펴본다. 또한 세 프레임워크에서 공통 테스트 설정 및 해체가 어떻게
지원되는지 또는 지원되지 않는지 등에 대해 자세히 설명한다.
Zope 프레임워크의 테스트 검색
관심이 있는 모듈의 목록을 결정한 후 모듈 내의 실제 테스트를 검색하려면 어떻게 해야 할까?
먼저 zope.testing 프레임워크를 살펴보면 Zope 커뮤니티의
흥미로운 특징을 알 수 있다. 이 커뮤니티에서는 여러 문제를 해결하는 큰 도구를 작성하는 대신
주로 서로 연결할 수 있는 기능을 갖춘 작고 제한된 도구를 작성하는 경향이 있다. 이러한 관점에서
zope.testing 모듈은 실제로 테스트를 검색할 수 있는 어떠한 메커니즘도
제공하지 않는다.
대신 zope.testing에서는 실행할 가치가 있는 각 모듈의
테스트를 찾아서 목록에 넣는 작업을 각 프로그래머의 몫으로 남겨 놓았다. 이 프레임워크는
각 테스트 모듈에서 한 가지 즉, test_suite() 함수만 검사한다.
왜냐하면 이 함수는 해당 모듈에 정의되어 있는 모든 테스트가 포함된 표준 unittest.TestSuite
클래스의 인스턴스를 리턴하기 때문이다.
zope.testing을 사용하는 프로그래머 중에는 test_suite()
함수에 있는 이 테스트 목록만을 수동으로 작성하고 관리하는 프로그래머도 있고 정의되어 있고
사용할 수 있는 테스트를 검색하기 위한 약간의 단축 기능을 제공하는 사용자 정의 코드를 작성하는
프로그래머도 있다. 하지만 가장 흥미로운 선택은 바로 또 하나의 Zope 패키지인 z3c.testsetup을
사용한다는 것이다. 이 패키지는 다른 최신 Python 테스트 프레임워크와 마찬가지로 패키지의
개별 테스트를 자동으로 검색하는 기능을 제공한다.
이 방법을 보면 Zope 프로그래머가 하나의 통합 솔루션을 만들지 않고 프레임워크의 구성
요소가 될 수 있는 빌딩 블록을 작성하는 경향이 짙다는 것을 다시 한번 확인할 수 있다. z3c.testsetup
패키지에는 테스트를 선택할 때 사용할 명령행 인터페이스나 테스트 결과를 표시할 때 사용할 출력
모듈이 없다. 대신 이러한 기능을 온전히 zope.testing에 의존한다.
실제로 z3c.testsetup 사용자는 일반적으로 zope.testing의
테스트 모듈 검색 기능을 거의 사용하지 않는다. 대신 zope.testing 알고리즘의
기본 동작 즉, test.py라는 이름의 모듈을 찾은 다음 전체 소스 트리 내에서
같은 이름을 가진 단 하나의 모듈을 제공하는 동작만을 그대로 두고서 알고리즘의 나머지 동작을 모두
제외시킨다. 다음 코드는 test.py를 보여 주는 간단한 예이다.
import z3c.testsetup
test_suite = z3c.testsetup.register_all_tests(my_package) |
이 코드는 zope.testing에서 테스트 검색 작업을 제외하고
z3c.testsetup 자체에서 제공하는 강력한 검색 메커니즘을 사용합니다.
register_all_tests() 함수에는 여러 가지 구성 옵션을
지정할 수 있다. 이 기사에서는 이 함수의 기본 동작에 대해서만 설명하므로 이에 대한 자세한
정보는 z3c.testsetup 설명서에서 볼 수 있다. 이 기사에서 설명하는
다른 모든 프레임워크와는 달리 z3c.testsetup은 기본적으로 패키지
내의 각 Python 모듈의 이름이 아닌 패키지의 컨텐츠를 검색한다. 즉, 패키지에 있는 모든 모듈과
모든 .txt 또는 .rst 파일을 검색하여
텍스트 내에 :Test-Layer:가 포함되어 있는 모듈이나 파일을 선택한다. 그런
다음 모듈 내의 모든 TestCase 클래스와 텍스트 파일 내의 모든 doctest 절을
결합하여 테스트 스위트를 작성한다.
:Test-Layer: 문자열을 사용하여 테스트가 있는
파일을 표시하는 방법은 흥미로운 메커니즘이다. 이 방법을 사용할 경우에는 새 프로그래머가
패키지의 파일을 찾아보면서 테스트의 위치를 찾기 위해서는 모든 파일을 하나하나 열어보거나
적어도 :Test-Layer: 문자열을 검색해야 한다는 단점이 있다. (z3c.testsetup이
같은 작업을 수행해야 하는 것은 말할 것도 없고 이로 인해 파일 이름에 대해서만 작동하는
프레임워크보다 느려지는가?)
마지막으로 Zope 테스트 프레임워크에서는 UnitTest 인스턴스
또는 doctest인 테스트만 지원된다는 점에 유의해야 한다. 이 시리즈의 첫 번째 기사에서 설명한
대로 최신 Python 테스트 프레임워크에서는 일반 Python 함수도 유효한 테스트로 지원된다. 이를
위해서는 앞으로 이러한 프레임워크를 살펴보면서 다룰 다른 테스트 검색 알고리즘이 필요하다.
py.test 및 nose의 테스트 검색
이전 기사에서 설명했던 대로 py.test 및
nose 프레임워크에서는 비슷하지만 약간 다른 규칙
세트를 사용하여 Python 패키지에서 테스트가 포함되었을 것으로 예상되는 모듈을
검색한다. 하지만 이러한 프레임워크 모두 검색해야 하는 모듈 목록을 사용하여
개발자가 테스트로 실행하려는 함수 및 클래스를 찾는다.
지난 기사에서 본 것처럼 py.test는 테스트를 사용하는
모든 프로젝트에서 따를 것으로 기대되는 단일 표준을 선택하는 반면 nose에서는
훨씬 강력한 사용자 정의를 수행하여 예측 가능한 동작을 대체한다. 이 경우에도 마찬가지로
py.test에서는 테스트 모듈 내의 테스트를 검색하는 규칙이 예측
가능하고 고정 불변인 반면 nose에서는 이러한 규칙이 유연하고
사용자 정의 가능하다. 프로젝트에서 nose를 테스트에 사용하는
경우에는 먼저 프로젝트의 setup.cfg 파일을 보고서 nose가
일반적인 규칙에 따라 테스트를 검색하는지 또는 이 개별 프로젝트와 관련된 다른 규칙을 따르는지
여부를 확인해야 한다.
다음은 py.test에서 수행되는 절차이다.
py.test는 Python 테스트 모듈의 내부를 검색하여
test_로 시작하는 모든 함수와 Test로
시작하는 모든 클래스를 수집한다. 클래스가 unittest.TestCase를
상속하는지 여부와 상관 없이 클래스를 수집한다.
- 테스트 함수는 간단히 실행하면 되지만 테스트 클래스는 메소드를 검색해야
한다. 클래스가 인스턴스화되면
test_로 시작하는 모든 메소드가
테스트로 실행된다.
py.test 프레임워크는 표준 Python unittest.TestCase
클래스를 상속한 테스트 클래스와 함께 실행할 경우 이상한 결과가 발생한다. 클래스에 여러 유용한 test_
메소드가 있더라도 runTest() 메소드가 없으면 예외가 발생하면서 py.test가
중단된다. 하지만 runTest() 메소드가 존재하더라도 py.test는 이 메소드를 무시한다.
왜냐하면 이 메소드는 클래스를 허용하기 위해 필요하지만 test_로 시작하지 않기 때문에
실행되지 않는다.
이 동작을 수정하려면 프레임워크의 conttest.py 파일이나
-p 명령행 옵션을 사용하여 프레임워크의 unittest
플러그인을 활성화해야 한다.
이 명령을 실행하면 py.test 동작의 세 부분이 변경된다.
먼저 Test로 시작하는 테스트 클래스만 검색하는 것이 아니라 unittest.TestCase를
상속한 다른 클래스도 검색한다. 두 번째로 py.test는 runTest()
메소드를 제공하지 않는 TestCase 서브클래스에 대한 예외를 더 이상 보고하지
않는다. 마지막 세 번째로 TestCase 서브클래스의 setUp()
및 tearDown() 메소드가 클래스에 포함된 테스트 이전 및 이후에 표준 방식에 따라
정상적으로 실행된다.
nose의 동작은 좀 더 많은 부분을 사용자 정의할 수 있지만
여기에서는 좀 더 간단해졌다.
nose는 Python 테스트 모듈의 내부를 검색하여
테스트 모듈을 선택하기 위한 정규 표현식과 일치하는 함수와 클래스를 수집한다. (기본적으로
nose는 Test 또는 test라는 단어가
포함된 이름을 찾는다. 하지만 명령행이나 구성 파일에서 다른 정규식을 제공할 수도 있다.)
nose는 테스트 클래스의 내부를 검색할 때 동일한
정규 표현식과 일치하는 메소드를 실행한다.
- 따로 요청하지 않는 한
nose는 항상 unittest.TestCase의
서브클래스를 검색하여 테스트로 사용한다. 하지만 nose는 표준 unittest 패턴인
^test를 사용하지 않고 고유한 정규 표현식을 사용하여 테스트로 사용할
메소드를 결정한다.
생성 테스트
첫 번째 기사에서 살펴본 것처럼 py.test와 nose에서는
다음과 같이 간단한 함수처럼 작성된 테스트가 지원되기 때문에 Python 테스트를 매우 쉽게 작성할 수 있다.
# test_new.py - simple tests functions
def testTrue(self):
assert True == 1
def testFalse(self):
assert False == 0 |
테스트 함수와 일반적인 테스트 클래스만 있어도 특정 단일 환경에서 구성 요소의
동작을 확인하는 작업을 충분히 수행할 수 있지만 대부분의 내용이 동일하고 일부 매개변수만
다른 수많은 테스트를 수행하려는 경우에는 매우 비효율적인 작업이 될 것이다.
테스트 함수를 수십번 복사하고 붙여넣으면서 고유한 이름으로 변경하지 않고도
그러한 테스트를 쉽게 구현할 수 있도록 py.test 와 nose
모두 생성 테스트를 지원한다. 이 방법의 핵심은 반복기의 역할을 수행하면서 yield
명령문을 사용하여 일련의 함수와 이러한 함수를 호출할 때 사용할 인수를 함께 리턴하는
테스트 함수를 제공하는 것이다. 예를 들어, 자주 사용하는 각각의 웹 브라우저에 대해
단일 테스트를 실행하기 위해 다음과 같은 테스트 코드를 작성할 수 있다.
# test_browser.py
def check(browser, page):
t = TestBrowser(browser)
t.load_page(page)
t.check_status(200)
def test_browsers():
for b in 'ie6', 'ie7', 'firefox', 'safari':
for p in 'index.html', 'about.html':
yield check, b, p |
py.test는 생성 테스트를 지원하기 위해 한 가지 편리한
기능을 제공한다. 이 기능을 활용하면 별도로 실행되는 테스트를 쉽게 구별할 수 있고 그 결과
실패한 테스트가 있을 경우 테스트 보고서를 파악할 수 있다. 예를 들어, 아래와 같이 사용자가
생성한 각 튜플의 첫 번째 항목은 테스트 이름의 일부로 인쇄되는 이름일 수 있다.
# Alternate yield statement, for py.test
...
yield 'Page %s browser %s' % (b,p), check, b, p |
생성 테스트는 직접 작성한 테스트를 사용하거나 테스트를 유닛테스트 기능으로
제한하는 많은 프로젝트의 수준 낮은 기술로 수행 중인 많은 테스트보다 매개변수화된
테스트를 위해 훨씬 효과적인 솔루션을 제공해야 한다.
설정 및 해체
공통 설정 및 해체 코드를 처리하는 방법은 테스트 스위트를 설계 및 작성할 때
고려해야 하는 중요한 사항이다. 실제로 사용되는 수많은 테스트는 이 기사에서 예제로
사용하고 있는 매우 간단한 함수와는 달리 매우 복잡하다. 실제 테스트에서는 Firefox에서
웹 페이지를 열고 "Continue"라는 레이블이 지정된 단추를 클릭한 후 결과를 검사하는 등의
작업을 수행해야 한다. 실제 테스트를 시작하기(페이지를 열고 단추를 클릭하기) 전에 먼저
몇 가지 중요한 단계를 완료해야 한다.
다음과 같은 테스트를 수행하는 100개의 기능 테스트가 있다고 간주하자. 각 테스트에서는
고유한 특정 테스트를 실행하기 전에 공통 설정 루틴을 호출하여 Firefox를 실행해야 한다. 이뿐만
아니라 설정 루틴에 의해 수행된 작업을 취소하기 위해 필요한 해체 코드도 있기 때문에 결국에는
테스트 스위트에서 이러한 부가적인 함수 호출이 200번 수행되어야 한다. 각 함수의 내용은 다음과 같다.
# How test functions look if they each do setup and teardown
def test_index_click_continue():
do_big_setup() # <- the same in every test
t = TestBrowser(browser)
t.load_page('index.html')
t.click('#continue')
t.check_status(200)
do_big_teardown() # <- the same in every test |
이처럼 반복되는 코드를 줄이기 위해 많은 테스트 프레임워크에는 전체 테스트
그룹별로 설정 및 해체 코드를 한 번만 실행하도록 지정할 수 있는 방법이 있다.
이 기사에서 살펴보고 있는 세 가지 프레임워크인 zope.testing,
py.test 및 nose에서도 프로그래머가
작성한 unittest.TestCase 클래스의 표준 setUp()
및 tearDown() 루틴을 지원한다. 하지만 이 점을 제외하고는 프레임워크마다
공통 설정 코드를 위해 매우 다른 기능을 제공한다.
zope.testing의 경우 자체적으로는 설정 및 해체를 위한
추가 지원을 제공하지 않지만 위에서 설명한 z3c.testsetup 확장을
통해 doctest와 관련된 작업을 수행한다. 앞에서 살펴본 대로 이 확장은 테스트 내에서 :Test-Layer:가
지정된 파일을 검색하여 테스트를 찾는다. doctest의 계층은 실제로 두 값 중 하나를 지정할 수
있다. doctest를 unit 계층에 속한 것으로 표시하는 것은 특별한
설정 없이 테스트가 실행된다는 것을 의미하는 반면 functional
계층에 속한 것으로 표시하는 것은 프레임워크 설정 함수가 호출된 이후에만 테스트가 실행된다는
것을 의미한다.
일반적으로 :Test-Layer: functional 테스트는 Zope 웹 프레임워크가
완전히 구성된 후에 실행되도록 설계되므로 테스트 브라우저 인스턴스를 작성하고, 요청을 보낸 후 웹
프레임워크에서 보내는 응답을 볼 수 있다. doctest를 대신하여 이 설정을 수행하면 z3c.testsetup에서
많은 양의 상용구 코드를 각각의 기능 doctest에 복사하지 않아도 된다.
마지막으로 각 유닛 doctest의 네임스페이스에 미리 로드할 변수 목록과 기능 doctest를
위해 미리 로드할 또 다른 변수 목록을 z3c.testsetup에 지정할 수
있으며 이 방법을 사용할 경우에도 상용구 코드가 줄어든다. 또한 모든 doctest 파일의 맨 위에
공통으로 들어가는 여러 import 명령문을 붙여넣지 않아도 된다.
이제 py.test를 살펴보자. 이 프레임워크는 기본적으로 설정
및 해체에 대한 지원을 제공하지 않는다. 그리고 unittest 플러그인을
사용하지 않는 한 표준 unittest.TestCase 클래스의 setUp()
및 tearDown() 메소드도 실행하지 않는다.
공통 테스트 코드를 가장 잘 지원하는 프레임워크는 nose이다. 테스트를
검색할 때 nose는 테스트가 검색된 컨텍스트를 추적한다. 이 프레임워크는 unittest.TestCase
서브클래스에 있는 모든 테스트 메소드가 해당 클래스 "내"에 있기 때문에 해당 setUp()
및 tearDown() 메소드에 의해 제어된다고 간주하며 테스트 또한 해당 모듈, 포함 패키지
및 외부의 임의 패키지 "내"에 있다고 간주한다. 따라서 nose의 경우 테스트는 하나의
컨테이너가 아닌 일련의 집중적 컨테이너 내에 있으며, 이러한 컨테이너는 테스트 전에 실행되는 설정 코드와 테스트
후에 실행되는 해체 코드를 포함할 수 있다.
nose 설명서에서 패키지 전체 및 모듈 전체에 적용되는
설정 및 해체 함수에 대한 자세한 정보를 볼 수 있으며, 무엇보다도 다양한 방법으로 설정 및
해체 함수를 호출할 수 있다는 것을 알 수 있을 것이다. (다시 한번 말하지만 nose의
경우에는 서로의 코드를 쉽게 읽을 수 있도록 하기 위해 다양한 프로젝트에서 동일한 방식으로
테스트를 작성하도록 권장하기가 어렵다.)
하지만 함수를 패키지 및 모듈로 그룹화할 수 있는 매우 강력한 방법이 있다. 이 그룹화는 구조적(모든
함수가 단일 구조에 포함됨)으로 뿐만 아니라 의미적(모든 테스트가 동일한 환경에서 실행됨)으로도
함수를 그룹화한다.
nose는 사용자가 @with_setup
데코레이터를 사용하여 특정 함수에 대한 설정 및 해체 함수를 명시적으로 지정한 경우에는
이들 함수의 이름에 간여하지 않는다. 이에 대한 자세한 정보는 nose
설명서에서 확인할 수 있다. 이 기사에서는 함수가 Python에서 가장 중요한 오브젝트이기 때문에
간략하게만 설명한다. 다음과 같이 특정 데코레이터에 이름을 할당한 후 반복해서 사용할 수 있다.
# Naming a with_setup decorator
firefox_test = with_setup(firefox_setup, firefox_teardown)
@firefox_test
def test_index_click():
...
@firefox_test
def test_index_menu():
... |
마지막으로 다음과 같은 차이점이 있다. @with_setup 데코레이터에
지정되거나 unittest.TestCase 서브클래스의 메소드로 제공되는 설정 및
해체 함수는 두 함수 사이에 있는 각 함수나 테스트에 대해 한 번씩만 실행되지만 모듈 또는 패키지
레벨에서 nose에 제공한 설정 및 해체 코드는 전체 테스트 세트에 대해 한 번만
실행된다. 따라서 그러한 테스트가 서로 올바르게 구별될 것으로 예상해서는 안된다. 이들 테스트는
모듈 또는 패키지의 설정 루틴에서 작성한 단일 리소스 사본을 공유한다.
결론
드디어 다양한 테스트 프레임워크에서 지원되는 테스트 검색 기능과 테스트
실행을 위한 준비 기능에 대한 정보를 모두 살펴보았으며 이러한 기능이 지원되지
않는 프레임워크도 있다는 것도 알아보았다. 이 시리즈의 마지막 기사에서는 프레임워크에서
테스트를 수집하기 위해 수행할 모든 작업에서 활용할 수 있는 기능 즉, 유용한 테스트 결과를
얻기 위해 사용할 수 있는 강력한 테스트 선택 옵션, 보고 도구 및 디버깅 지원에 대해 살펴본다. 그리고
결론에서는 이러한 프레임워크 중에서 요구 사항에 가장 적합한 프레임워크를 선택하는 방법에 대해 설명한다.
참고자료 교육
토론
필자소개  | |  | Brandon Craig Rhodes는 Python Magazine의 편집장으로 활동하고 있으며 10년 이상의 Python 언어 경력을
가지고 있는 독립 웹 애플리케이션 컨설턴트이다. 업계 수준의 계산 천문학 루틴에 대한 오브젝트 지향적 인터페이스를
제공하는 PyEphem 확장 모듈을 직접 개발하여 9년 이상 관리해 오고 있으며 여러 대륙의 천문학자들이 이 확장 모듈을
사용하고 있다. Brandon은 Python Atlanta 사용자 그룹의 조정자이기도 하다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|