☜ 제 12 장 SOAP 웹서비스 """ Dive Into Python """
다이빙 파이썬
제 14 장 테스트-먼저 프로그래밍 ☞

제 13 장 유닛 테스트

13.1. 로마 숫자 소개

앞장에서는 “바로 뛰어 들어가” 코드를 살펴보고 가능하면 재빨리 이해하려고 시도했습니다. 이제 어느 정도는 파이썬을 익혔으므로, 뒤로 한 걸음 물러서서 코드가 작성되기 전에 일어난 단계들을 살펴보겠습니다.

다음 몇 장에 걸쳐서 로마 숫자를 변환하는 유틸리티 함수 집합을 작성하고 디버그하며 그리고 최적화해 보겠습니다. 로마 숫자를 구성하고 평가하는 내부작동방식은 섹션 7.3 “사례 연구: 로마 숫자”에서 보셨지만 이제는 뒤로 물러서서 양-방향 유틸리티로 확장하려면 무엇이 필요할지 생각해 보겠습니다.

로마 숫자 규칙을 보면 재미있는 사실들이 관찰됩니다:

  1. 특정 숫자를 로마 숫자로 표현하려면 오직 올바르게 한가지 방법만 있다.
  2. 그 반대도 역시 참이다: 문자열이 올바른 로마 숫자라면, 오직 하나의 숫자만 표현한다 (즉, 오직 한 가지 방식으로만 읽힌다).
  3. 로마 숫자는 표현 범위가 한정되어 있다. 구체적으로 1에서부터 3999까지이다. (로마인들은 여러가지 방식으로 거대한 수를 표현했다. 예를 들면 숫자 위에 막대를 하나 두어 그의 정상적인 값이 1000배가 되어야 함을 나타냈지만 여기에서는 그를 다루지 않는다. 이 장의 목적을 위하여 로마 숫자는 범위가 1에서부터 3999까지라고 가정하자.)
  4. 로마 숫자로 0은 표현할 방법이 없다. (놀랍게도 고대 로마인들은 0이 숫자라는 개념이 없었다. 숫자는 가지고 있는 물건을 세기 위한 것이었다; 가지고 있지 않은 것을 어떻게 세겠는가?)
  5. 로마 숫자로 음수를 표현할 방법이 없다.
  6. 로마 숫자로 분수 즉 정수가-아닌-숫자를 표현할 방법이 없다.

이 모든 것을 고려해 보면 로마 숫자를 변환하는 함수 집합을 만드는데 무엇이 예상됩니까?

roman.py의 요구조건

  1. toRoman1에서 3999까지의 모든 정수에 대하여 로마 숫자 표현을 돌려준다.
  2. toRoman은 주어진 정수가 1에서 3999 사이의 범위를 벗어나면 실패해야 한다.
  3. toRoman은 정수가-아닌-숫자가 주어지면 실패해야 한다.
  4. fromRoman은 유효한 로마 숫자를 받아 그가 표현하는 숫자를 돌려주어야 한다.
  5. fromRoman은 무효한 로마 숫자가 주어지면 실패해야 한다.
  6. 숫자를 취해 그것을 로마 숫자로 변환한 다음 그것을 다시 숫자로 변환하면 처음 시작한 숫자로 돌아와야 한다. 그래서 1..3999 범위의 모든 n에 대하여 fromRoman(toRoman(n)) == n이어야 한다.
  7. toRoman은 언제나 대문자를 사용하여 로마 숫자를 돌려주어야 한다.
  8. fromRoman은 오직 로마 숫자를 대문자로만 받아들여야 한다 (즉, 소문자로 입력되면 실패해야 한다).

더 읽어야 할 것

  • 로마와 다른 문명이 실제로 어떻게 로마 숫자를 사용했는지에 관한 매력적인 역사를 비롯하여, 이 사이트에 로마 숫자에 관한 정보가 더 많이 있다 (짧은 대답: 우연하게 그리고 제멋대로 사용됨).

13.2. 뛰어 들기

이제 변환 함수에 예상되는 행위들을 완벽하게 정의하였으므로, 약간 예상치 못한 일을 해 보겠습니다: 다음 함수들을 시험해 보고 여러분이 바라는 대로 행위하도록 보증하는 테스트 모듬을 작성할 것입니다. 제대로 읽으셨습니다: 아직 작성하지 않은 코드를 테스트하는 코드를 작성할 것입니다.

이를 유닛 테스트라고 부릅니다. 왜냐하면 나중에 더 큰 프로그램의 일부가 되겠지만 따로 떼어서, 두개의 변환 함수 세트를 유닛 단위로 테스트하고 작성하기 때문입니다. 파이썬은 적절하게-이름붙인 unittest 모듈이라는 유닛 테스트를 위한 작업틀이 있습니다.

unittest파이썬 2.1 이상의 버전에 포함되어 있습니다. 파이썬 2.0 사용자는 pyunit.sourceforge.net에서 내려받으시면 됩니다.

유닛 테스트는 전반적인 테스트-중심 개발 전략에서 중요한 부분을 차지합니다. 유닛 테스트를 작성하면 일찍 작성하는 것이 중요하고 (테스트될 코드 작성보다 우선하는 것이 더 좋음), 코드와 요구조건이 바뀌자마자 바로바로 갱신하는 것이 중요합니다. 유닛 테스트는 더-높은 수준의 함수적 테스트 또는 시스템 테스트를 대체하지 않으며, 오히려 모든 개발 국면에서 중요합니다:

  • 코드를 작성하기 전에, 요구조건을 쓸모가 있게 자세하게 기술하지 않을 수 없다.
  • 코드를 작성하면서, 과도하게 코딩하지 않아도 된다. 테스트 사례가 모두 통과하면 함수는 완성된다.
  • 코드를 리팩토링할 때, 새 버전이 예전 버전과 똑 같이 작동한다고 보증할 수 있다.
  • 코드를 유지보수할 때, 누군가 달려와서 최근 변경 때문에 예전 코드가 망가졌다고 항의할 때 변호하는데 도움을 준다. (“하지만 사장님, 제가 체크했을 때는 모든 유닛 테스트가 통과했는데요...”)
  • 팀 단위로 코드를 작성할 때, 내가 제출하려는 코드가 다른 이의 코드를 망가트리지 않을 것이라는 자신감이 증진된다. 왜냐하면 유닛테스트를 먼저 실행할 수 있기 때문이다. (이런 종류의 일을 코드 경연(sprint)에서 보았다. 팀은 주어진 과제를 나누어서, 각자 주어진 과업에 대한 상세를 받아, 그에 대한 유닛 테스트를 작성한 다음, 자신의 유닛 테스트를 나머지 팀원과 공유한다. 그런 식으로 해서, 아무도 경계를 넘어서서 다른 이와 잘 어울리지 않는 코드를 개발하지 않게 된다.)

13.3. romantest.py 소개

다음은 로마 숫자 변환 함수를 위한 완전한 테스트 모듬입니다. 로마 숫자 변환 함수는 아직 작성되지는 않았지만 결국 roman.py에 포함될 것입니다. 어떻게 이 모든 것이 하나로 결합될지는 지금 당장은 잘 모릅니다; 이 클래스나 메쏘드 어느 것도 다른 것들을 전혀 참조하지 않습니다. 여기에는 충분한 이유가 있으며, 잠시 후에 가르쳐 드리겠습니다.

예제 13.1. romantest.py

아직 그렇게 하지 못했다면 이 책에 사용된 이 예제와 다른 예제들을 내려 받을 수 있습니다.

"""roman.py를 위한 유닛테스트"""

import roman
import unittest

class KnownValues(unittest.TestCase):                          
    knownValues = ( (1, 'I'),
                    (2, 'II'),
                    (3, 'III'),
                    (4, 'IV'),
                    (5, 'V'),
                    (6, 'VI'),
                    (7, 'VII'),
                    (8, 'VIII'),
                    (9, 'IX'),
                    (10, 'X'),
                    (50, 'L'),
                    (100, 'C'),
                    (500, 'D'),
                    (1000, 'M'),
                    (31, 'XXXI'),
                    (148, 'CXLVIII'),
                    (294, 'CCXCIV'),
                    (312, 'CCCXII'),
                    (421, 'CDXXI'),
                    (528, 'DXXVIII'),
                    (621, 'DCXXI'),
                    (782, 'DCCLXXXII'),
                    (870, 'DCCCLXX'),
                    (941, 'CMXLI'),
                    (1043, 'MXLIII'),
                    (1110, 'MCX'),
                    (1226, 'MCCXXVI'),
                    (1301, 'MCCCI'),
                    (1485, 'MCDLXXXV'),
                    (1509, 'MDIX'),
                    (1607, 'MDCVII'),
                    (1754, 'MDCCLIV'),
                    (1832, 'MDCCCXXXII'),
                    (1993, 'MCMXCIII'),
                    (2074, 'MMLXXIV'),
                    (2152, 'MMCLII'),
                    (2212, 'MMCCXII'),
                    (2343, 'MMCCCXLIII'),
                    (2499, 'MMCDXCIX'),
                    (2574, 'MMDLXXIV'),
                    (2646, 'MMDCXLVI'),
                    (2723, 'MMDCCXXIII'),
                    (2892, 'MMDCCCXCII'),
                    (2975, 'MMCMLXXV'),
                    (3051, 'MMMLI'),
                    (3185, 'MMMCLXXXV'),
                    (3250, 'MMMCCL'),
                    (3313, 'MMMCCCXIII'),
                    (3408, 'MMMCDVIII'),
                    (3501, 'MMMDI'),
                    (3610, 'MMMDCX'),
                    (3743, 'MMMDCCXLIII'),
                    (3844, 'MMMDCCCXLIV'),
                    (3888, 'MMMDCCCLXXXVIII'),
                    (3940, 'MMMCMXL'),
                    (3999, 'MMMCMXCIX'))                       

    def testToRomanKnownValues(self):                          
        """toRoman은 알려진 입력에 알려진 결과를 산출해야 한다"""
        for integer, numeral in self.knownValues:              
            result = roman.toRoman(integer)                    
            self.assertEqual(numeral, result)                  

    def testFromRomanKnownValues(self):                          
        """fromRoman은 알려진 입력에 알려진 결과를 산출해야 한다"""
        for integer, numeral in self.knownValues:                
            result = roman.fromRoman(numeral)                    
            self.assertEqual(integer, result)                    

class ToRomanBadInput(unittest.TestCase):                            
    def testTooLarge(self):                                          
        """toRoman은 거대 입력에 실패해야 한다"""                   
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, 4000)

    def testZero(self):                                              
        """toRoman은 입력이 0이면 실패해야 한다"""                       
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0)   

    def testNegative(self):                                          
        """toRoman은 음수 입력에 실패해야 한다"""                
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1)  

    def testNonInteger(self):                                        
        """toRoman은 정수-아닌 입력에 실패해야 한다"""             
        self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5) 

class FromRomanBadInput(unittest.TestCase):                                      
    def testTooManyRepeatedNumerals(self):                                       
        """fromRoman은 너무 많이 반복되는 숫자에 실패해야 한다"""              
        for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):             
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)

    def testRepeatedPairs(self):                                                 
        """fromRoman은 숫자 쌍이 반복되면 실패해야 한다"""              
        for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):               
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)

    def testMalformedAntecedent(self):                                           
        """fromRoman은 모양이 나쁜 선행자에 실패해야 한다"""                   
        for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
                  'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):                       
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)

class SanityCheck(unittest.TestCase):        
    def testSanity(self):                    
        """fromRoman(toRoman(n))==n for all n"""
        for integer in range(1, 4000):       
            numeral = roman.toRoman(integer) 
            result = roman.fromRoman(numeral)
            self.assertEqual(integer, result)

class CaseCheck(unittest.TestCase):                   
    def testToRomanCase(self):                        
        """toRoman은 언제나 대문자로 돌려주어야 한다"""  
        for integer in range(1, 4000):                
            numeral = roman.toRoman(integer)          
            self.assertEqual(numeral, numeral.upper())

    def testFromRomanCase(self):                      
        """fromRoman은 오직 대문자로만 입력을 받아야 한다"""
        for integer in range(1, 4000):                
            numeral = roman.toRoman(integer)          
            roman.fromRoman(numeral.upper())          
            self.assertRaises(roman.InvalidRomanNumeralError,
                              roman.fromRoman, numeral.lower())

if __name__ == "__main__":
    unittest.main()   

더 읽어야 할 것

13.4. 성공을 위한 테스트

유닛 테스트에서 가장 근본적인 부분은 테스트 사례마다 따로따로 구성하는 것입니다. 테스트 사례는 테스트중인 코드에 관하여 단 하나의 질문에만 답합니다.

테스트 사례는 다음을 할 수 있어야 합니다...

  • ...인간의 입력 없이, 스스로 완벽하게 실행된다. 유닛 테스트는 자동화에 관한 것이다.
  • ...인간이 그 결과를 해석할 필요없이, 테스트중인 함수가 실패할지 통과할지 스스로 결정한다.
  • ...다른 테스트 사례와 분리되어, 따로 실행된다 (같은 함수들을 테스트할지라도 말이다). 각 테스트 사례는 고립된 섬이다.

위의 사항을 고려해서, 첫 테스트 사례를 구축해 보겠습니다. 요구조건은 다음과 같습니다:

  1. toRoman1에서 3999 사이의 모든 정수에 대하여 로마 숫자를 돌려주어야 한다.

예제 13.2. testToRomanKnownValues

class KnownValues(unittest.TestCase):                           
    knownValues = ( (1, 'I'),
                    (2, 'II'),
                    (3, 'III'),
                    (4, 'IV'),
                    (5, 'V'),
                    (6, 'VI'),
                    (7, 'VII'),
                    (8, 'VIII'),
                    (9, 'IX'),
                    (10, 'X'),
                    (50, 'L'),
                    (100, 'C'),
                    (500, 'D'),
                    (1000, 'M'),
                    (31, 'XXXI'),
                    (148, 'CXLVIII'),
                    (294, 'CCXCIV'),
                    (312, 'CCCXII'),
                    (421, 'CDXXI'),
                    (528, 'DXXVIII'),
                    (621, 'DCXXI'),
                    (782, 'DCCLXXXII'),
                    (870, 'DCCCLXX'),
                    (941, 'CMXLI'),
                    (1043, 'MXLIII'),
                    (1110, 'MCX'),
                    (1226, 'MCCXXVI'),
                    (1301, 'MCCCI'),
                    (1485, 'MCDLXXXV'),
                    (1509, 'MDIX'),
                    (1607, 'MDCVII'),
                    (1754, 'MDCCLIV'),
                    (1832, 'MDCCCXXXII'),
                    (1993, 'MCMXCIII'),
                    (2074, 'MMLXXIV'),
                    (2152, 'MMCLII'),
                    (2212, 'MMCCXII'),
                    (2343, 'MMCCCXLIII'),
                    (2499, 'MMCDXCIX'),
                    (2574, 'MMDLXXIV'),
                    (2646, 'MMDCXLVI'),
                    (2723, 'MMDCCXXIII'),
                    (2892, 'MMDCCCXCII'),
                    (2975, 'MMCMLXXV'),
                    (3051, 'MMMLI'),
                    (3185, 'MMMCLXXXV'),
                    (3250, 'MMMCCL'),
                    (3313, 'MMMCCCXIII'),
                    (3408, 'MMMCDVIII'),
                    (3501, 'MMMDI'),
                    (3610, 'MMMDCX'),
                    (3743, 'MMMDCCXLIII'),
                    (3844, 'MMMDCCCXLIV'),
                    (3888, 'MMMDCCCLXXXVIII'),
                    (3940, 'MMMCMXL'),
                    (3999, 'MMMCMXCIX'))                        

    def testToRomanKnownValues(self):                           
        """toRoman should give known result with known input"""
        for integer, numeral in self.knownValues:              
            result = roman.toRoman(integer)                      
            self.assertEqual(numeral, result)                   
테스트 사례를 작성하기 위해, 먼저 unittest 모듈의 TestCase 클래스를 상속받습니다. 이 클래스는 유용한 메쏘드들을 많이 제공합니다. 특정한 조건을 테스트하기 위해 테스트 사례에 이를 사용할 수 있습니다.
이는 정수/숫자 쌍으로 구성된 리스트로서 수작업으로 검증했습니다. 여기에는 1에서 10까지의 수, 가장 큰 수, 한개짜리 로마 숫자로 변환되는 숫자 모두, 그리고 기타 유효한 숫자의 무작위 샘플이 포함되어 있습니다. 유닛 테스트의 요점은 가능한 모든 입력을 테스트하는게 아니라, 대표적인 샘플을 테스트하는 것입니다.
개별 테스트마다 자신만의 메쏘드가 있습니다. 이 메쏘드는 매개변수도 받지 않고 반환값도 없습니다. 이 메쏘드가 예외를 일으키지 않고 정상적으로 종료하면 테스트는 통과된 것으로 간주됩니다; 메쏘드가 예외를 일으키면 테스트는 실패한 것으로 간주됩니다.
여기에서 실제 toRoman 함수를 호출합니다. (자, 함수는 아직 작성되지 않았지만 작성된다면 이 줄에서 그 함수를 호출합니다.) 이제 toRoman 함수에 대하여 API를 정의했음을 주목하세요: (변환할 숫자인) 정수를 받고 (로마 숫자 표현인) 문자열을 돌려주어야 합니다. API가 그와 다르다면 이 테스트는 실패한 것으로 간주됩니다.
또 주목하세요. toRoman을 호출할 때 아무 예외도 잡지 않고 있습니다. 이는 의도적입니다. toRoman은 올바른 입력을 가지고 호출할 때 예외를 일으키면 안됩니다. 그리고 이 입력 값들은 모두 유효합니다. toRoman 함수가 예외를 일으키면 이 테스트는 실패한 것으로 간주됩니다.
toRoman 함수가 올바르게 정의되었고, 올바르게 호출되며, 성공적으로 완료되었고, 값을 하나 돌려준다고 간주합니다. 마지막 단계는 함수가 옳은 값을 돌려주는지 점검하는 것입니다. 이는 흔한 질문입니다. 그리고 TestCase 클래스는 assertEqual이라는 메쏘드를 제공하는데, 두 값이 동일한지 점검할 수 있습니다. toRoman으로부터 반환된 결과(result)가 예상하고 있는 알려진 값(numeral)에 일치하지 않으면 assertEqual는 예외를 일으키고 테스트는 실패합니다. 두 값이 같으면 assertEqual은 아무 일도 하지 않습니다. toRoman으로부터 돌아오는 값이 예상한 값에 일치하면 assertEqual는 절대로 예외를 일으키지 않습니다. 그래서 testToRomanKnownValues는 결국 정상적으로 종료하며, 이는 toRoman이 이 테스트를 통과했다는 뜻입니다.

13.5. 실패를 위한 테스트

좋은 입력이 주어질 때 함수가 성공하는지 테스트하는 것만으로는 충분하지 않습니다; 나쁜 입력이 주어지면 함수가 실패하는지도 반드시 테스트해야 합니다. 그리고 예상치 못한 그런 종류의 실패가 아니라; 반드시 예상한대로 실패해야 합니다.

toRoman에 대한 기타 요구조건:

  1. toRoman1에서 3999까지의 범위를 벗어난 정수가 주어지면 실패해야 한다.
  2. toRoman은 정수-아닌-숫자가 주어지면 실패해야 한다.

파이썬에서 함수는 실패를 나타내기 위해 예외를 일으킵니다. 나쁜 입력이 주어지면 함수가 특정한 예외를 일으키는지 테스트하기 위한 메쏘드가 unittest 모듈에 제공됩니다.

예제 13.3. toRoman 함수에다 나쁜 입력 테스트

class ToRomanBadInput(unittest.TestCase):                            
    def testTooLarge(self):                                          
        """toRoman should fail with large input"""                   
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, 4000) 

    def testZero(self):                                              
        """toRoman should fail with 0 input"""                       
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0)    

    def testNegative(self):                                          
        """toRoman should fail with negative input"""                
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1)  

    def testNonInteger(self):                                        
        """toRoman should fail with non-integer input"""             
        self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5)  
unittestTestCase 클래스는 assertRaises 메쏘드를 제공하는데, 이 메쏘드는 다음 인자를 받습니다: 예상하는 예외와 테스트할 함수 그리고 함수에 건넬 인자들을 받습니다. (테스트할 함수가 여러 인자를 받을 경우, 그 모두를 순서대로 assertRaises에 건네면 곧바로 테스트 중인 함수에 건넵니다.) 여기에서 무엇을 하고 있는지 세심하게 주의를 기울이세요: (try...except 블록에 싸 넣어서) toRoman을 직접 호출하고 특정 예외가 일어나는지 수작업으로 점검하는 대신에, assertRaises는 그 모든 일을 캡슐화했습니다. 거기에다 예외 (roman.OutOfRangeError)와 함수 (toRoman) 그리고 toRoman의 인자들 (4000)을 건네기만 하면 되며, 그러면 assertRaises가 알아서 toRoman을 호출하고 확실하게 roman.OutOfRangeError을 일으키는지 점검해 줍니다.
(또 toRoman 함수 자체를 인자로 건네고 있음을 주목하세요; 호출하고 있지 않으며, 그 이름을 문자열로 건네지 않습니다. 기억하십니까? 최근에 저는 함수와 예외를 비롯하여, 파이썬에서 모든 것은 객체다라는 사실이 얼마나 편리한지 언급한 바 있습니다.)
너무 큰 숫자를 테스트하는 것과 더불어, 너무 작은 숫자도 테스트할 필요가 있습니다. 기억하세요. 로마 숫자는 0이나 음수를 표현할 수 없습니다. 그래서 그에 대하여 각각 테스트 사례를 가집니다 (testZero 그리고 testNegative). testZero에서는 toRoman0을 가지고 호출할 때 roman.OutOfRangeError 예외를 일으키는지 테스트하고 있습니다; roman.OutOfRangeError 예외가 일어나지 않으면(실제 값을 돌려주거나 또는 기타 다른 예외를 일으키면), 이 테스트는 실패한 것을 간주됩니다.
요구조건 #3에 의하면 toRoman은 정수-아닌-숫자를 받을 수 없습니다. 그래서 여기에서 0.5을 가지고 호출하면 toRoman이 확실하게 roman.NotIntegerError 예외를 일으키는지 테스트합니다. toRomanroman.NotIntegerError를 일으키지 않으면 이 테스트는 실패한 것으로 간주됩니다.

toRoman 대신 fromRoman에 적용된다는 점만 제외하면 다음 두 요구조건은 앞의 세 조건과 비슷합니다:

  1. fromRoman은 유효한 로마 숫자를 하나 받아 그가 표현하는 숫자를 돌려주어야 한다.
  2. fromRoman은 무효한 로마 숫자가 주어지면 실패해야 한다.

요구조건 #4는 요구조건 #1과 똑 같은 방식으로 처리되는데, 알려진 값들로 구성된 샘플을 순회하면서 순서대로 각 값을 테스트합니다. 요구조건 #5는 요구조건 #2 그리고 #3과 같은 방식으로 처리됩니다. 즉 일련의 나쁜 입력 값들을 테스트하여 fromRoman이 적절한 예외를 일으키는지 확인합니다.

예제 13.4. fromRoman 함수에다 나쁜 입력 테스트

class FromRomanBadInput(unittest.TestCase):                                      
    def testTooManyRepeatedNumerals(self):                                       
        """fromRoman should fail with too many repeated numerals"""              
        for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):             
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s) 

    def testRepeatedPairs(self):                                                 
        """fromRoman should fail with repeated pairs of numerals"""              
        for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):               
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)

    def testMalformedAntecedent(self):                                           
        """fromRoman should fail with malformed antecedents"""                   
        for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
                  'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):                       
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
이에 관해서는 별로 말할 것이 없습니다; 패턴은 toRoman에 나쁜 입력을 테스트하는데 사용한 것과 정확하게 똑 같습니다. 또다른 예외가 있다고 간략하게 언급하겠습니다: roman.InvalidRomanNumeralError이 있습니다. (roman.OutOfRangeError 그리고 roman.NotIntegerError와 더불어) 이를 합쳐 총 세개의 맞춤 예외를 roman.py에 정의할 필요가 있습니다. 나중에 이 장에서, 실제로 roman.py를 작성할 때, 이 세개의 맞춤 예외를 정의하는 법을 보여드리겠습니다.

13.6. 위생을 위한 테스트

종종, 코드 한 단위에서 상반적인 함수 세트가 포함되어 있는 경우가 있습니다. 보통 변환 함수의 형태로 들어 있는데 한 함수는 A에서 B로 변환하고 또 한 함수는 B를 A로 변환합니다. 이런 경우, “위생 점검”을 두어서 A를 B로 그리고 그 반대로 변환할 수 있는지 확인하면 유용합니다. 정밀도를 잃어 버리지 말아야 하고 반올림 에러를 야기하지 말아야 하며 또는 기타 다른 종류의 버그를 촉발시키면 안됩니다.

다음 요구조건을 고려해 보겠습니다:

  1. 숫자를 하나 받아, 그것을 로마 숫자로 변환한 다음, 다시 숫자로 변환하더라도 결국 처음 시작한 숫자로 끝나야 한다. 그래서 1..3999 사이의 모든 n에 대하여 fromRoman(toRoman(n)) == n이어야 한다.

예제 13.5.  fromRoman에 대하여 toRoman 테스트

class SanityCheck(unittest.TestCase):        
    def testSanity(self):                    
        """fromRoman(toRoman(n))==n for all n"""
        for integer in range(1, 4000):         
            numeral = roman.toRoman(integer) 
            result = roman.fromRoman(numeral)
            self.assertEqual(integer, result) 
앞서 range 함수를 보셨지만 여기에는 두 개의 인자로 호출되고, 첫 인자(1)에서 시작하여 연속적으로 두 번째 인자 미만(4000)까지 정수 리스트를 돌려줍니다. 그리하여 1..3999가 되는데, 이는 로마 숫자로 변환하기 위하여 유효한 범위입니다.
정수(integer)는 파이썬에서 키워드가 아니라는 것을 언급하고 싶었습니다; 여기에서는 다른 것과 마찬가지로 변수 이름일 뿐입니다.
여기에서 실제 테스트 로직은 보이는 그대로입니다: 숫자(integer)를 취해, 그것을 로마 숫자(numeral)로 변환한 다음, 다시 그것을 숫자(result)로 변환해서 결과가 처음 시작한 숫자와 같은지 확인합니다. 같지 않다면 assertEqual 예외를 일으키고 테스트는 즉시 실패한 것으로 간주됩니다. 모든 숫자가 일치하면 assertEqual는 언제 조용히 반환되고, 전체 testSanity 메쏘드도 결국 조용히 반환되며, 테스트는 통과된 것으로 간주됩니다.

뒤의 두 요구조건은 다른 것과 다른데 임의적일 뿐만 아니라 사소하기 때문입니다:

  1. toRoman은 언제나 대문자를 사용하여 로마 숫자를 돌려주어야 한다.
  2. fromRoman은 오직 대문자로된 로마 숫자만 받아 들인다 (다시 말해, 소문자가 입력되면 실패해야 한다).

실제로, 약간 제멋대로입니다. 예를 들면 fromRoman이 소문자와 혼합 입력도 받아들인다고 가정할 수 있습니다. 그러나 완전히 임의적이지는 않습니다; toRoman이 언제나 대문자로 출력을 돌려준다면 fromRoman은 적어도 대문자 입력을 받아들여야 합니다. 그렇지 않으면 “위생 점검” (요구조건 #6)은 실패합니다. 대문자로만 입력을 받아들인다는 사실은 임의적인 것이지만, 어떤 시스템에서든 대소문자는 문제가 됩니다. 그래서 처음부터 행위를 지정할 가치가 있습니다. 지정할 가치가 있다면 테스트할 가치가 있습니다.

예제 13.6. 대소문자 테스트

class CaseCheck(unittest.TestCase):                   
    def testToRomanCase(self):                        
        """toRoman should always return uppercase"""  
        for integer in range(1, 4000):                
            numeral = roman.toRoman(integer)          
            self.assertEqual(numeral, numeral.upper())         

    def testFromRomanCase(self):                      
        """fromRoman should only accept uppercase input"""
        for integer in range(1, 4000):                
            numeral = roman.toRoman(integer)          
            roman.fromRoman(numeral.upper())                    
            self.assertRaises(roman.InvalidRomanNumeralError,
                              roman.fromRoman, numeral.lower())   
이 테스트 사례에서 가장 흥미로운 것은 테스트되지 않는 모든 것입니다. toRoman으로부터 반환된 값이 올바른지 심지어 일관성이 있는지조차 테스트하지 않습니다; 그런 질문은 별도의 테스트 사례로 답합니다. 대문자인지 테스트하는 온전한 테스트 사례가 있습니다. 이를 위생 점검과 결합하고 싶은 유혹이 드실텐데, 두 방법 모두 전체 범위의 값을 순회하면서 toRoman을 호출하기 때문입니다.[6] 그러나 그렇게 하면 기본 규칙중 하나를 범하게 됩니다: 테스트 사례는 하나당 오직 하나의 질문에만 답해야 합니다. 이 대소문자 점검과 위생 점검을 결합했는데, 테스트 사례가 실패했다고 생각해 보세요. 그러면 더 분석해 들어가 테스트 사례의 어느 부분에서 문제가 무엇인지 결정하지 못했는지 알아내야할 필요가 있습니다. 유닛 테스트의 결과를 분석하여 그것이 무슨 뜻인지 알아야 내야할 필요가 있다면 그것은 테스트 사례를 잘못 디자인 했다는 확실한 징표입니다.
여기에서 배워야할 비슷한 교훈이 하나 있습니다: toRoman이 언제나 대문자를 돌려준다는 것을 “알고 있다고 할지라도”, 여기에서는 명시적으로 그의 반환 값을 대문자로 변환하여 fromRoman이 대문자 입력을 받아들이는지 테스트합니다. 왜일까? 왜냐하면 toRoman이 언제나 대문자를 돌려준다는 사실은 독립적인 요구조건이기 때문입니다. 예를 들어 그 요구조건을 바꿔서 언제나 소문자를 돌려주도록 했다면 testToRomanCase 테스트 사례는 바꿀 필요가 있겠지만 이 테스트 사례는 여전히 작동하기 때문입니다. 이는 또다른 기본 규칙입니다: 테스트 사례는 다른 테스트 사례와 별도로 작동할 수 있어야 합니다. 테스트 사례는 나홀로 섬입니다.
fromRoman의 반환 값을 어떤 것에도 할당하지 않고 있음에 주목하세요. 이는 파이썬에서 합법적인 구문입니다; 함수가 값을 돌려주지만 아무도 신경쓰지 않는다면 파이썬은 그냥 그 반환 값을 버립니다. 이 경우, 버리고 있습니다. 이 테스트 사례는 반환 값에 대하여 아무것도 테스트하지 않습니다; 그저 fromRoman이 예외를 일으키지 않고 대문자 입력을 받아들이는지 테스트할 뿐입니다.
이 줄은 복잡하지만 ToRomanBadInput 테스트와 FromRomanBadInput 테스트에서 했던 작업과 아주 비슷합니다. 여기에서 테스트는 특정 함수(roman.fromRoman)를 특정 값(회돌이 안에서 현재 로마 숫자를 소문자로, numeral.lower())을 가지고 호출하면 특정 예외(roman.InvalidRomanNumeralError)를 일으키는지 확인합니다. (회돌이를 돌 때마다) 그렇다면 테스트는 통과합니다; 한 번이라도 뭔가 다른 일을 하면 (다른 예외를 일으키거나, 또는 예외는 일으키지 않으나 값을 돌려준다면), 테스트는 실패합니다.

다음 장에서는 이런 테스트를 통과하는 코드를 작성하는 법을 살펴보겠습니다.



[6] 유혹만 제외하면 나는 무엇이든 참을 수 있다.” -- 오스카 와일드(Oscar Wilde)

☜ 제 12 장 SOAP 웹서비스 """ Dive Into Python """
다이빙 파이썬
제 14 장 테스트-먼저 프로그래밍 ☞