제 4 장 HTML 처리 | 목 차 | 부 록 >> |
이 전 장에서는 “다이빙해 들어가서” 직접적으로 코드를 살펴보고 되도록이면 빨리 이해하려고 노력하였다. 이제 허리춤에 파이썬을 차게 되었으므로, 뒤로 돌아가 그 코드가 작성되기 전의(before) 단계들을 살펴 보겠다.
이 장에서는 로마 숫자로부터 그리고 로마숫자로 변환하는 유틸리티 집합을 작성하려고 한다. 혹시 인식하지는 못한다고 할지라도, 분명히 로마 숫자들을 보았을 것이다. 오래된 영화나 텔레비젼 쇼에서 복사권리 표식을 보았을 것이다 ( “Copyright 1946”대신에 “Copyright MCMXLVI”을), 혹은 도서관이나 혹은 대학의 봉헌 벽에서 ( “established 1888” 대신에 “established MDCCCLXXXVIII” )을 보았으리라. 또한 요약목록과 전기 참조서에서 보았을 수도 있다. 로마 숫자는 숫자 표현의 시스템으로서 고대로 거슬로 올라가 로마 시대에 실제로 사용되었다 (거기에서 그 이름이 유래한다).
로마 숫자는 7개의 문자가 있어서 여러가지 방식으로 반복되고 조합되어 숫자들을 표현한다.
로마 숫자를 구성하는데는 몇가지 규칙이 있다:
이러한 규칙들을 보면 수 많은 재미있는 사실들을 관찰할 수 있다:
이러한 모든 것을 전제로 하여, 로마숫자로 그리고 로마숫자로부터 변환하는 함수의 모임에서 무엇을 예상할 수 있는가?
roman.py의 요구조건
더 읽어야 할 것
이제 변환 함수로부터 예상할 수 있는 행위를 완전하게 정의하였으므로, 약간은 예상치 못한 어떤 일들을 하려고 한다: 테스트 모둠을 작성하여 이러한 함수들이 자기의 길을 가도록 하고 우리가 원하는 대로 그들이 움직이는 것을 확인하려고 한다. 여러분이 읽은 대로다: 지금까지 작성하지 않았던 테스트 코드를 작성하려고 한다.
이것을 유닛 테스트라고 부르는데, 왜냐하면 두 개의 변환 함수들의 모임이, 커다란 프로그램으로부터 분리되어져, 유닛으로 작성되어지고 테스트 될 수 있기 때문이다. 유닛은 이후에 그 큰 프로그램의 일 부분이 될 것이다. 파이썬은 유닛 테스트를 위한 작업틀을 가지고 있다. 그 이름에 걸맞게 unittest라는 모듈을 가진다.
![]() | |
unittest는 파이썬 2.1 이후에서 사용가능하다. 파이썬 2.0 사용자는 pyunit.sourceforge.net로부터 내려받을 수있다. |
유닛 테스트는 전체 테스트-중심 개발 전략의 가장 중요한 부분이다. 유닛 테스트를 작성한다면, 빨리 작성하는 것이 중요하다 (특히 테스트할 코드를 작성하기 전이라면 더욱 좋다). 그리고 코드와 요구사항이 변함에 따라서 그것들을 계속하여 갱신하라. 유닛 테스트는 고-수준의 기능적 혹은 시스템 테스트를 위한 대체물이 아니라, 오히려 모든 개발의 국면에서 중요하다:
이것은 로마 숫자 변환 함수들을 위한 완전한 테스트 모둠이다. 아직 작성되지 않았지만 결국 roman.py에 씌여지게 될 것이다. 어떻게 그 모든 것들이 조립될 지는 직접적으로 확실하게 보이지는 않는다; 이러한 클래스 혹은 메쏘드중 어느 것도 다른 것을 참조하지 않는다. 이러한 것에는 충분한 이유가 있으며, 그것을 간략하게 살펴 보겠다.
아직 그렇게 하지 못했다면, 이 예제와 함께 이 책에서 사용된 다른 예제들을 내려 받을 수 있다 (Windows, UNIX, Mac OS).
"""Unit test for 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 should give known result with known input""" for integer, numeral in self.knownValues: result = roman.toRoman(integer) self.assertEqual(numeral, result) def testFromRomanKnownValues(self): """fromRoman should give known result with known input""" for integer, numeral in self.knownValues: result = roman.fromRoman(numeral) self.assertEqual(integer, result) 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 testDecimal(self): """toRoman should fail with non-integer input""" self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5) 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) 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 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()) if __name__ == "__main__": unittest.main()
더 읽어야 할 것
유닛 테스트의 가장 기본적인 부분은 개별적인 테스트 사례를 구성하는 것이다. 하나의 테스트 사례는 테스트되고 있는 코드에 관한 한 개의 질문에 답해 준다.
테스트 사례는 ....를 할 수 있어야 한다
이것을 전제로 하여, 첫 번째 테스트 사례를 구축해보자. 요구조건은 다음과 같다:
Example 5.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)
함수에 좋은 입력이 주어질 때 성공하는 것을 테스트하는 것만으로는 충분하지 않다; 또한 나쁜 입력이 주어질 때도 실패한다는 것을 테스트 해야 한다. 그리고 단지 어떤 종류의 실패가 아니라; 예상한 방식대로 실패해야한다.
toRoman를 위한 다른 요구 조건을 기억하라 :
파이썬에서 함수는 예외를 일으킴으로써, 실패를 나타낸다. 그리고 unittest 모듈은 함수에 나쁜 입력값이 주어지면 특별한 예외를 일으키는지 테스트를 하기 위한 메쏘드를 제공한다.
Example 5.3. Testing bad input to 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 testDecimal(self): """toRoman should fail with non-integer input""" self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5)
![]() | unittest의 TestCase 클래스는 assertRaises 메쏘드를 제공하는데, 다음의 인자들을 취한다: 예상되는 예외, 테스트하려는 함수, 그리고 함수에 건네주는 인자들. (테스트하려는 함수가 한개 이상의 인자를 가지면, 순서대로 그들 모두를 assertRaises에 넘겨 주면 된다. 그러면 그들을 정확히 연이어서 테스트하고 있는 그 함수에 건네 줄 것이다.) 여기에서 하고 있는 것에 주의를 기울여 보라: toRoman을 직접적으로 호출하고 (그것을 try...except 블록에 싸 넣음으로써) 수작업으로 그것이 특별한 예외를 일으키는지 점검하는 대신에, assertRaises는 그 모든 것들을 우리 대신에 캡슐화한다. 우리가 하는 모든 것은 그것에 예외(roman.OutOfRangeError), 함수(toRoman), 그리고 toRoman의 인자(4000)들을 주는 것이다. assertRaises는 주의하여 toRoman을 호출하고 그리고 그것이 roman.OutOfRangeError를 일으키는지 확실하게 점검한다. (함수 그리고 예외를 포함하여 파이썬에서 모든 것은 객체이다라는 것이 얼마나 유용한지 내가 최근에 언급한 일을 기억하는가?) |
![]() | 아주 큰 숫자들을 테스트하면서, 아주 작은 숫자들을 테스트할 필요가 있다. 기억하라. 로마 숫자는 0 또는 음의 정수를 표현할 수 없다. 그래서 그러한 각각에 대하여 테스트 사례를 가진다 (testZero 그리고 testNegative). testZero에서 toRoman이 0으로 호출될 때 roman.OutOfRangeError 예외를 일으키는지 테스트한다; 만약 roman.OutOfRangeError를 일으키지 않는다면 (그것이 실제의 값을 반환하거나 혹은 어떤 다른 예외를 일으키기 때문에), 이 테스트는 실패로 간주된다. |
![]() | 요구조건 #3은 toRoman이 비-정수 소수를 받아들일 수 없다고 가정한다. 그래서 여기에서 toRoman이 소수(0.5)로 호출될 때 roman.NotIntegerError 예외를 일으키는지 확인하는 테스트를 한다. 만약 toRoman이 roman.NotIntegerError를 일으키지 않는다면, 이 테스트는 실패로 간주된다. |
다음의 두 요구조건들은 toRoman대신에 fromRoman에 적용된다는 점만 빼고는, 처음 세 개와 비슷하다:
요구조건 #4는 requirement #1과 같은 방식으로 처리되어, 알려진 값들의 샘플을 반복하여서 순서대로 각각을 테스트한다. 요구조건 #5는 요구조건 #2, #3과 같은 방식으로 처리되어, 일련의 나쁜 입력을 테스트하고 fromRoman이 그 적절한 예외를 일으키는지 확인한다.
Example 5.4. Testing bad input to 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)
한 단위의 코드가 보통 변환함수들의 형태에서 상호호출적인 함수들의 집합을 포함하고 있는 것을 발견하는 경우가 자주 있다. 거기에서 하나는 A를 B로 변환하고 다른 하나는 B를 A로 변환한다. 이러한 경우에, 유용한 것은 “청결 점검(sanity check)”를 작성하여 A에서 B로 그리고 역으로 A로, 소수의 정밀도를 잃어버리지 않고, 버림 에러를 일으키지 않고, 혹은 다른 어떤 종류의 버그도 촉발시키지 않고, 변환할 수 있다는 것을 확인하는 것이다.
이런 요구조건을 생각해 보라
Example 5.5. Testing toRoman against fromRoman
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 메쏘드는 결국 조용하게 돌아올 것이고, 테스트는 통과된 것으로 간주 될 것이다. |
마지막 두 개의 요구조건은 다른 것들과는 다른데 왜냐하면 그것들은 둘 다 임의적이고 사소하기 때문이다:
사실은 약간은 임의적이다. 예를 들어, fromRoman은 소문자와 대소문자가 섞인 입력을 받아들인다고 가정한다. 그러나 완전히 임의적인 것은 아니다; 만약 toRoman이 항상 대문자로 출력을 반환한다면, fromRoman은 적어도 대문자의 입력을 받아들여야만 한다. 그렇지 않으면 “청결 점검(sanity check)” (요구조건 #6)은 실패할 것이다. '그것은 오직(only) 대문자의 입력만을 받아 들인다'라고 하는 사실은 임의적이다. 그러나 모든 시스템 통합회로가 경고하듯이, 대소문자는 항상 문제거리이다. 그래서 그 행위를 전면에 지정할 가치가 있다. 그리고 지정할 만한 가치가 있다면, 테스트할 가치도 있다
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을 호출함으로써, 청결 점검 (sanity check)과 결합하고 싶은 유혹에 빠질 지도 모르겠다.[11] 그러나 그것은 기본적인 규칙들을 위반할 것이다: 각 테스트 사례는 오직 한개의 질문에만 답하여야 한다. 이 사례 점검을 그 청결 점검과 결합하고, 그리고 그 테스트 사례가 실패한다고 상상해 보라. 더 깊이 분석을 해서 그 테스트 사례의 어느 부분이 어떤 문제가 있는지를 결정하는데 실패했는지 연구하여야 할 것이다. 유닛 테스트의 결과를 분석하여 단지 그것들이 무엇을 뜻하는지를 연구해야 한다면, 그것은 테스트 사례를 잘못-디자인했다는 확실한 표시이다. |
![]() | 여기에서 배워야할 비슷한 교훈이 있다: toRoman는 항상 대문자를 반환한다는 것을 “알고 있음에도” 불구하고, 명시적으로 그것의 반환 값을 대문자로 변환하여 fromRoman이 대문자 입력을 받아들이는 지 테스트한다. 왜인가? 왜냐하면 'toRoman이 항상 대문자를 반환한다는 것'은 독립적인 요구조건이기 때문이다. 예를 들어, 항상 소문자를 반환하도록 요구조건을 변경하면, testToRomanCase 테스트 사례는 변경되어야 할 것이다. 그러나 이 테스트 사례는 여전히 작동할 것이다. 이것은 또 다른 기본적인 규칙이다: 각 테스트 사례는 다른 모든 것과는 별개로 작동할 수 있어야 한다. 모든 테스트 사례는 고립된 섬이다. |
![]() | fromRoman의 반환 값을 어디에도 할당하지 않는다는 것을 주목하라. 파이썬에서 이것은 적법한 구문이다; 만약 한 함수가 값을 반환함에도 불구하고 아무도 관심을 기울이지 않으면, 파이썬은 단지 그 반환 값을 버릴 뿐이다. 이 경우에 그것이 우리가 원한 바이다. 이 테스트 사례는 반환 값에 대하여 아무 것도 테스트하지 않는다; 단지 fromRoman이 아무런 예외없이 대문자 입력을 받아들이는지를 테스트할 따름이다. |
이제 유닛 테스트가 완성되었으므로, 테스트 사례가 테스트하기를 시도하고 있는 바로 그 코드를 작성하기 시작할 시간이다. 단계별로 이렇게 하려고 한다. 그래서 모든 유닛 테스트가 실패하는 것을 볼 수 있다. 우리가 roman.py안의 공백들을 메꾸어 감에 따라서 하나씩 하나씩 그것들이 통과하는 것을 볼 수 있다.
아직 그렇게 하지 않았다면, 이 예제와 이 책에서 사용되는 다른 예제들을 내려 받을 수 있다 (Windows, UNIX, Mac OS).
"""Convert to and from Roman numerals""" #Define exceptions class RomanError(Exception): passclass OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass
def toRoman(n): """convert integer to Roman numeral""" pass
def fromRoman(s): """convert Roman numeral to integer""" pass
![]() | 이것이 바로 자신만의 맞춤 예외를 파이썬에서 정의하는 방법이다. 예외는 클래스이다. 그리고 존재하는 예외들을 하부클래스화 함으로써 자신만의 예외를 생성한다. (필수적인 것은 아니지만) Exception을 하부클래스화 하기를 강력히 권장한다. 그것은 모든 내장 예외들이 상속되는 기본 클래스이다. 여기에서는 (Exception으로부터 상속된) RomanError를 정의하는데, 그것은 기본 클래스로 작용하여 다른 나의 모든 맞춤 예외들이 따라온다. 이것은 스타일의 문제이다; 나는 Exception 클래스로부터 직접적으로 아주 쉽게 개별적인 예외를 따로따로 상속받을 수 있었다. |
![]() | OutOfRangeError와 NotIntegerError 예외는 결국 toRoman에 의하여 사용되어져, ToRomanBadInput에 지정된 대로, 다양한 형태의 무효한 입력을 들어올려 신호할 것이다. |
![]() | InvalidRomanNumeralError 예외는 결국 fromRoman에 의해 사용되어져서, FromRomanBadInput에 지정된 대로, 무효한 입력을 들어올려 신호할 것이다. |
![]() | 이 단계에서, 함수 각각을 위한 API를 작성하기를 원하지만, 아직 코딩하기를 원하지는 않는다. 그래서 파이썬의 예약어 pass를 사용하여 임시로 세운다. |
이제 그 결정적인 순간 (드럼을 쳐주세요 두두두~): 마침내 유닛 테스트를 이 임시적인 작은 모듈에 대하여 실행하려고 한다. 이 시점에서, 모든 테스트 사례는 실패해야 한다. 사실, 어떤 테스트라도 제 1 단계를 통과한다면, romantest.py로 돌아가야만 하고 그리고 왜 아무것도-하지-않는 함수들을 가지고 통과할 정도로 쓸모 없는 테스트를 작성했는지를 재평가해야 한다.
romantest1.py을 -v 명령어-줄 선택사항으로 실행하라. 더욱 자세한 출력을 보여 줄 것이고 그래서 각 테스트 사례가 실행되는 동안 정확하게 무엇이 진행되고 있는 볼 수 있다. 운이 좋다면, 출력은 다음과 비슷해야 한다:
Example 5.8. Output of romantest1.py against roman1.py
fromRoman should only accept uppercase input ... ERROR toRoman should always return uppercase ... ERROR fromRoman should fail with malformed antecedents ... FAIL fromRoman should fail with repeated pairs of numerals ... FAIL fromRoman should fail with too many repeated numerals ... FAIL fromRoman should give known result with known input ... FAIL toRoman should give known result with known input ... FAIL fromRoman(toRoman(n))==n for all n ... FAIL toRoman should fail with non-integer input ... FAIL toRoman should fail with negative input ... FAIL toRoman should fail with large input ... FAIL toRoman should fail with 0 input ... FAIL ====================================================================== ERROR: fromRoman should only accept uppercase input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 154, in testFromRomanCase roman1.fromRoman(numeral.upper()) AttributeError: 'None' object has no attribute 'upper' ====================================================================== ERROR: toRoman should always return uppercase ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 148, in testToRomanCase self.assertEqual(numeral, numeral.upper()) AttributeError: 'None' object has no attribute 'upper' ====================================================================== FAIL: fromRoman should fail with malformed antecedents ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 133, in testMalformedAntecedent self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with repeated pairs of numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 127, in testRepeatedPairs self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with too many repeated numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 122, in testTooManyRepeatedNumerals self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 99, in testFromRomanKnownValues self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ====================================================================== FAIL: toRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 93, in testToRomanKnownValues self.assertEqual(numeral, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: I != None ====================================================================== FAIL: fromRoman(toRoman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 141, in testSanity self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ====================================================================== FAIL: toRoman should fail with non-integer input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 116, in testDecimal self.assertRaises(roman1.NotIntegerError, roman1.toRoman, 0.5) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: NotIntegerError ====================================================================== FAIL: toRoman should fail with negative input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 112, in testNegative self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, -1) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ====================================================================== FAIL: toRoman should fail with large input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 104, in testTooLarge self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 4000) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ====================================================================== FAIL: toRoman should fail with 0 input---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 108, in testZero self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 0) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError
---------------------------------------------------------------------- Ran 12 tests in 0.040s
FAILED (failures=10, errors=2)
이제 roman 모듈의 작업틀이 준비되었으므로, 코드를 작성하고 테스트 사례를 넘겨줄 시간이다.
아직 그렇게 하지 못하였다면, 이 예제와 함께 이 책에서 사용된 다른 예제들을 내려 받을 수 있다 (Windows, UNIX, Mac OS).
"""Convert to and from Roman numerals""" #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Define digit mapping romanNumeralMap = (('M', 1000),('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) def toRoman(n): """convert integer to Roman numeral""" result = "" for numeral, integer in romanNumeralMap: while n >= integer:
result += numeral n -= integer return result def fromRoman(s): """convert Roman numeral to integer""" pass
Example 5.10. How toRoman works
어떻게 toRoman이 작동하는지 잘 모르겠다면, print 서술문을 while 회돌이의 마지막에 추가하라:
while n >= integer: result += numeral n -= integer print 'subtracting', integer, 'from input, adding', numeral, 'to output'
>>> import roman2 >>> roman2.toRoman(1424) subtracting 1000 from input, adding M to output subtracting 400 from input, adding CD to output subtracting 10 from input, adding X to output subtracting 10 from input, adding X to output subtracting 4 from input, adding IV to output 'MCDXXIV'
그래서 toRoman는 적어도 우리의 임시 수동 점검에서는 작동하는 듯이 보인다. 그러나 유닛 테스트를 통과할까? 음 아니다, 전적으로는 아니다.
Example 5.11. Output of romantest2.py against roman2.py
fromRoman should only accept uppercase input ... FAIL toRoman should always return uppercase ... okfromRoman should fail with malformed antecedents ... FAIL fromRoman should fail with repeated pairs of numerals ... FAIL fromRoman should fail with too many repeated numerals ... FAIL fromRoman should give known result with known input ... FAIL toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... FAIL toRoman should fail with non-integer input ... FAIL
toRoman should fail with negative input ... FAIL toRoman should fail with large input ... FAIL toRoman should fail with 0 input ... FAIL
![]() | toRoman은 사실 언제나 대문자를 반환한다. 왜냐하면 romanNumeralMap이 로마 숫자 표현들을 대문자로 정의하기 때문이다. 그래서 이 테스트는 이미 통과한 거나 다름없다. |
![]() | 여기에 엄청난 뉴스가 있다: toRoman 함수의 이 버전은 알려진 값 테스트를 통과한다. 기억하라. 일반적이지는 않지만, 다양한 좋은 입력을 가지고 그 함수의 능력을 평가한다. 그 입력에 포함되는 것은 한개짜리-문자의 모든 로마 숫자를 산출하는 입력들, 가능한 가장 큰 입력 (3999), 그리고 가능한 가장 긴 로마 숫자 (3888)를 산출하는 입력이다. 이 시점에서는 그 함수가 던져 넣는 어떠한 좋은 입력값에도 작동한다고 논리적으로 확신할 수 있다. |
![]() | 그렇지만 그 함수는 나쁜 값에 대하여는“작동하지” 않는다; 모든 개별적 나쁜 입력 테스트에 실패한다. 그것은 당연하다. 왜냐하면 나쁜 입력에 대하여 어떤 점검부도 포함하지 않았기 때문이다. 그러한 테스트 사례는 (assertRaises를 통하여) 특정한 예외가 일어나기를 기다린다. 다음 단계에서 그렇게 해 보겠다. |
다음은 유닛 테스트의 출력결과이다. 모든 실패들에 관한 세부사항을 나열한다. 10개의 에러를 맞이했다.
====================================================================== FAIL: fromRoman should only accept uppercase input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 156, in testFromRomanCase roman2.fromRoman, numeral.lower()) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with malformed antecedents ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 133, in testMalformedAntecedent self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with repeated pairs of numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 127, in testRepeatedPairs self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with too many repeated numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 122, in testTooManyRepeatedNumerals self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 99, in testFromRomanKnownValues self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ====================================================================== FAIL: fromRoman(toRoman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 141, in testSanity self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ====================================================================== FAIL: toRoman should fail with non-integer input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 116, in testDecimal self.assertRaises(roman2.NotIntegerError, roman2.toRoman, 0.5) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: NotIntegerError ====================================================================== FAIL: toRoman should fail with negative input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 112, in testNegative self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, -1) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ====================================================================== FAIL: toRoman should fail with large input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 104, in testTooLarge self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 4000) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ====================================================================== FAIL: toRoman should fail with 0 input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 108, in testZero self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 0) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ---------------------------------------------------------------------- Ran 12 tests in 0.320s FAILED (failures=10)
이제 toRoman이 (1에서 3999까지의 정수의) 좋은 입력으로는 정확하게 행동하므로, (다른 모든) 나쁜 입력값으로도 정확하게 행동하도록 만들 시간이다.
아직 그렇게 하지 못하였다면, 이 예제와 더불어 이 책에서 사용된 다른 예제들을 내려 받을 수 있다 (Windows, UNIX, Mac OS).
"""Convert to and from Roman numerals""" #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Define digit mapping romanNumeralMap = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) def toRoman(n): """convert integer to Roman numeral""" if not (0 < n < 4000):raise OutOfRangeError, "number out of range (must be 1..3999)"
if int(n) <> n:
raise NotIntegerError, "decimals can not be converted" result = ""
for numeral, integer in romanNumeralMap: while n >= integer: result += numeral n -= integer return result def fromRoman(s): """convert Roman numeral to integer""" pass
Example 5.13. Watching toRoman handle bad input
>>> import roman3 >>> roman3.toRoman(4000) Traceback (most recent call last): File "<interactive input>", line 1, in ? File "roman3.py", line 27, in toRoman raise OutOfRangeError, "number out of range (must be 1..3999)" OutOfRangeError: number out of range (must be 1..3999) >>> roman3.toRoman(1.5) Traceback (most recent call last): File "<interactive input>", line 1, in ? File "roman3.py", line 29, in toRoman raise NotIntegerError, "decimals can not be converted" NotIntegerError: decimals can not be converted
Example 5.14. Output of romantest3.py against roman3.py
fromRoman should only accept uppercase input ... FAIL toRoman should always return uppercase ... ok fromRoman should fail with malformed antecedents ... FAIL fromRoman should fail with repeated pairs of numerals ... FAIL fromRoman should fail with too many repeated numerals ... FAIL fromRoman should give known result with known input ... FAIL toRoman should give known result with known input ... okfromRoman(toRoman(n))==n for all n ... FAIL toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok
![]() | toRoman은 여전히 알려진 값 테스트를 통과하는데, 참으로 즐거운 일이다. 제 2 단계에서 통과된 모든 테스트도 여전히 통과한다. 그래서 마지막 코드는 아무것도 망가트리지 않았다. |
![]() | 더욱 신나는 일은 나쁜 입력 테스트가 모두 이제 통과한다는 것이다. 이 테스트 testDecimal는 int(n) <> n의 점검부 때문에 통과한다. 소수가 toRoman에 넘겨질 때, int(n) <> n 점검부는 그것을 알아채고 NotIntegerError 예외를 일으키는데, 그것이 바로 testDecimal이 기다리고 있는 것이다. |
![]() | 이 테스트 testNegative는 not (0 < n < 4000) 점검부 때문에 통과한다. 그것은 OutOfRangeError 예외를 일으키는데, 그것이 바로 testNegative가 기다리고 있는 것이다. |
====================================================================== FAIL: fromRoman should only accept uppercase input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 156, in testFromRomanCase roman3.fromRoman, numeral.lower()) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with malformed antecedents ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 133, in testMalformedAntecedent self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with repeated pairs of numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 127, in testRepeatedPairs self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with too many repeated numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 122, in testTooManyRepeatedNumerals self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 99, in testFromRomanKnownValues self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ====================================================================== FAIL: fromRoman(toRoman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 141, in testSanity self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ---------------------------------------------------------------------- Ran 12 tests in 0.401s FAILED (failures=6)
![]() | |
일반적인 유닛 테스트가 여러분에게 알려줄 수 있는 가장 중요한 것은 언제 코딩을 멈추는가이다. 한 함수를 위한 모든 유닛 테스트가 통과될 때, 그 함수를 코딩하는 것을 중지하라. 전체 모듈에 대한 모든 유닛 테스트가 통과될 때, 그 모듈을 코딩하는 것을 중지하라. |
이제 toRoman이 완성되었으므로, fromRoman을 코딩할 시간이다. 개별적인 로마 숫자를 정수 값으로 짝지워 주는 풍부한 데이타 구조 덕분에, 이것은 toRoman 함수와 마찬가지로 어렵지 않다.
아직 그렇게 하지 못했다면, 이 예제와 그리고 이 책에서 사용된 다른 예제들을 내려 받을 수 있다 (Windows, UNIX, Mac OS).
"""Convert to and from Roman numerals""" #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Define digit mapping romanNumeralMap = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) # toRoman function omitted for clarity (it hasn't changed) def fromRoman(s): """convert Roman numeral to integer""" result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral:result += integer index += len(numeral) return result
![]() | 여기에서의 패턴은 toRoman과 동일하다. 로마 숫자 데이타 구조(터플들의 터플)를 회돌이 하여, 가능한 한 가장 높은 정수 값에 짝지우는 대신에, 가능한 한 “가장 높은” 로마 숫자 문자열에 짝지운다. |
Example 5.16. How fromRoman works
어떻게 fromRoman이 작동하는지 이해하지 못하겠다면, print 서술문을 while 회돌이의 뒤에다 추가하라:
while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) print 'found', numeral, ', adding', integer
>>> import roman4 >>> roman4.fromRoman('MCMLXXII') found M , adding 1000 found CM , adding 900 found L , adding 50 found X , adding 10 found X , adding 10 found I , adding 1 found I , adding 1 1972
Example 5.17. Output of romantest4.py against roman4.py
fromRoman should only accept uppercase input ... FAIL toRoman should always return uppercase ... ok fromRoman should fail with malformed antecedents ... FAIL fromRoman should fail with repeated pairs of numerals ... FAIL fromRoman should fail with too many repeated numerals ... FAIL fromRoman should give known result with known input ... oktoRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok
![]() | 여기에 두 개의 신나는 소식이 있다. 첫 째는 fromRoman이 좋은 입력에, 적어도 우리가 테스트하는 그 알려진 값들에 대하여 작동한다는 것이다. |
![]() | 둘 째는 청결 점검(sanity check) 역시 통과했다는 것이다. 알려진 값 테스트와 결합해보면, toRoman과 fromRoman 둘 다 가능한 모든 좋은 값들에 대하여 적절히 작동할 것이라고 논리적으로 확신할 수 있다. (이것이 보장되지는 않는다; 이론적으로 가능한 것은 toRoman이 버그를 가지고 있어서 특별한 입력의 집합에 대하여 잘못된 로마 숫자를 산출할 수도 있다는 것이며, 그리고 fromRoman이 상호호출적인 버그를 가지고 있어서, toRoman이 부적절하게 생성한 로마 숫자의 집합과 정확하게 똑같이, 잘못된 정수 값을 산출할지도 모른다는 것이다. 여러분의 어플리케이션과 요구조건에 따라서, 이 가능성은 여러분을 괴롭힐 수도 있다; 만약 그렇다면, 그것이 여러분을 괴롭히지 않을 때까지 보다 더 포괄적인 테스트 사례를 작성하라.) |
====================================================================== FAIL: fromRoman should only accept uppercase input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 156, in testFromRomanCase roman4.fromRoman, numeral.lower()) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with malformed antecedents ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 133, in testMalformedAntecedent self.assertRaises(roman4.InvalidRomanNumeralError, roman4.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with repeated pairs of numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 127, in testRepeatedPairs self.assertRaises(roman4.InvalidRomanNumeralError, roman4.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with too many repeated numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 122, in testTooManyRepeatedNumerals self.assertRaises(roman4.InvalidRomanNumeralError, roman4.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ---------------------------------------------------------------------- Ran 12 tests in 1.222s FAILED (failures=4)
이제 fromRoman이 좋은 입력으로는 적절하게 작동하므로, 그 퍼즐의 마지막 조각을 맞추어 넣어야 할 시간이다: 나쁜 입력으로 적절히 작동하도록 할 시간이다. 그것은 문자열을 보는 방법을 찾는 것을 의미하며 그리고 유효한 로마 숫자인지를 결정하는 방법을 찾는 것을 의미한다. 이것은 toRoman에서 수치 입력을 평가하는 것보다 본질적으로 더 어렵다. 그러나 우리에게는 마음대로 사용할 수 있는 강력한 도구가 있다: 정규 표현식이라는 도구가 있다.
만약 정규 표현식에 익숙하지 못하고 그리고 정규 표현식 101을 읽지 않았다면, 지금이 바로 좋은 기회가 될 것이다.
이 장의 서반부에서 본 바와 같이, 로마 숫자 하나를 구성하는 데에는 약간의 간단한 규칙이 있다. 첫 번째는 천단위 자리는, 만약 있다면, 일련의 M 문자에 의해서 표현된다.
Example 5.18. Checking for thousands
>>> import re >>> pattern = '^M?M?M?$'>>> re.search(pattern, 'M')
<SRE_Match object at 0106FB58> >>> re.search(pattern, 'MM')
<SRE_Match object at 0106C290> >>> re.search(pattern, 'MMM')
<SRE_Match object at 0106AA38> >>> re.search(pattern, 'MMMM')
>>> re.search(pattern, '')
<SRE_Match object at 0106F4A8>
백의 자리는 천의 자리보다 더욱 어렵다. 왜냐하면, 그 값에 따라서 상호 배타적으로 표현될 수 있는 여러 방법들이 있기 때문이다.
그래서 네 개의 가능한 패턴이 있다:
마지막 두 개의 패턴은 조합될 수 있다:
Example 5.19. Checking for hundreds
>>> import re >>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'>>> re.search(pattern, 'MCM')
<SRE_Match object at 01070390> >>> re.search(pattern, 'MD')
<SRE_Match object at 01073A50> >>> re.search(pattern, 'MMMCCC')
<SRE_Match object at 010748A8> >>> re.search(pattern, 'MCMC')
>>> re.search(pattern, '')
<SRE_Match object at 01071D98>
휴우! 정규 표현식이 얼마나 빨리 골치아파질 수 있는지 알겠는가? 단지 백의 자리와 천의 자리만을 다루었을 뿐이다. (이 장의 후반부에서 정규 표현식을 작성하는 약간 다른 구문을 보게 될 것이다. 똑 같이 복잡하기는 하겠지만, 적어도 그것은 그 표현식의 다른 부분에 줄-안 문서화를 허용한다.) 다행스럽게도, 그 모든 것을 따라간다면, 십의 자리와 일의 자리는 쉽다. 왜냐하면 정확히 같은 패턴이기 때문이다.
아직 그렇게 하지 않았다면, 이 예제와 함께 이 책에서 사용된 다른 예제들을 내려 받을 수 있다 (Windows, UNIX, Mac OS).
"""Convert to and from Roman numerals""" import re #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Define digit mapping romanNumeralMap = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) def toRoman(n): """convert integer to Roman numeral""" if not (0 < n < 4000): raise OutOfRangeError, "number out of range (must be 1..3999)" if int(n) <> n: raise NotIntegerError, "decimals can not be converted" result = "" for numeral, integer in romanNumeralMap: while n >= integer: result += numeral n -= integer return result #Define pattern to detect valid Roman numerals romanNumeralPattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'def fromRoman(s): """convert Roman numeral to integer""" if not re.search(romanNumeralPattern, s):
raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) return result
이 시점에서 거대하고 못생긴 정규 표현식이 모든 종류의 무효한 로마 숫자를 나포할 수 있다는 것을 의심해도 좋다. 그러나 내 말을 곧이 곧대로 믿지 마라. 그 결과를 살펴 보라:
Example 5.21. Output of romantest5.py against roman5.py
fromRoman should only accept uppercase input ... oktoRoman should always return uppercase ... ok fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 12 tests in 2.864s OK
![]() | 정규 표현식에 관하여 언급하지 않은 한 가지는 기본적으로 정규 표현식은 대소문자에-민감 하다는 것이다. 정규 표현식 romanNumeralPattern이 대문자로 표현되었으므로, re.search 점검은 완전하게 대문자가 아닌 어떤 입력도 거부할 것이다. 그래서 대문자 입력 테스트는 통과한다. |
![]() | 더욱 중요한 것은 나쁜 입력 테스트는 통과한다는 것이다. 예를 들어, 그 못생긴 조상 테스트 사례는 MCMC와 같은 사례를 점검한다. 보시다시피, 이것은 정규 표현식에 일치하지 않는다. 그래서 fromRoman은 InvalidRomanNumeralError 예외를 일으킨다. 그것은 그 못생긴 조상 테스트 사례가 기다리던 것이고, 그래서 그 테스트는 통과한다. |
![]() | 사실 모든 나쁜 입력 테스트는 통과한다. 이 정규 표현식은 테스트 사례를 만들 때 생각할 수 있었던 모든 것을 나포한다 . |
![]() | 그리고 올해의 최종 영광은 단어 “OK”에게로 돌아 간다; 모든 테스트가 통과될 때 unittest 모듈에 의하여 출력된다. |
![]() | |
모든 테스트가 통과될 때, 코딩을 중지하라. |
최선을 다하여 포괄적인 유닛 테스트를 작성하려고 하였음에도 불구하고, 버그는 일어난다. “bug”란 무엇인가? 버그는 여러분이 아직까지 작성한 적이 없는 테스트 사례이다.
>>> import roman5 >>> roman5.fromRoman("")0
![]() | 기억하는가? 이전의 섹션에서 빈 문자열이 유효한 로마 숫자를 점검하기 위한 정규 표현식에 일치하는 것을 보았다. 자, 이것은 그 정규 표현식의 최종 버전에 대하여도 역시 참이라는 것이다. 그리고 그것은 버그이다; 유효한 로마 숫자를 표현하지 않는 다른 어떤 연속적인 문자들과 마찬가지로 똑 같이 InvalidRomanNumeralError 예외를 일으킬, 빈 문자열을 원한다. |
버그를 재생산한 후에, 그리고 그것을 수정하기 전에, 실패를 하는 그리하여 그 버그를 보여 주는 테스트 사례를 작성하여야 한다.
Example 5.23. Testing for the bug (romantest61.py)
class FromRomanBadInput(unittest.TestCase): # previous test cases omitted for clarity (they haven't changed) def testBlank(self): """fromRoman should fail with blank string""" self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, "")![]()
![]() |
여기에 대단히 간단한 재료가 있다. fromRoman을 빈 문자열로 호출하고 그리고 그것이 InvalidRomanNumeralError 예외를 일으키는지 확인하라. 어려운 부분이라면 버그를 찾는 것이다; 이제 그것에 관하여 알고 있으므로, 그것을 위한 테스트는 쉬운 부분이다. |
우리의 코드는 버그를 가지고 있으므로, 그리고 이제 이 버그를 테스트하는 테스트 사례를 가지고 있으므로 테스트 사례는 실패할 것이다:
Example 5.24. Output of romantest61.py against roman61.py
fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok fromRoman should fail with blank string ... FAIL fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok ====================================================================== FAIL: fromRoman should fail with blank string ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage6\romantest61.py", line 137, in testBlank self.assertRaises(roman61.InvalidRomanNumeralError, roman61.fromRoman, "") File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ---------------------------------------------------------------------- Ran 13 tests in 2.864s FAILED (failures=1)
이제 버그를 고칠 수 있다.
Example 5.25. Fixing the bug (roman62.py)
def fromRoman(s): """convert Roman numeral to integer""" if not s:raise InvalidRomanNumeralError, 'Input can not be blank' if not re.search(romanNumeralPattern, s): raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) return result
![]() |
오직 두 줄의 코드만 있으면 된다: 빈 문자열을 위한 명시적인 점검, 그리고 raise 서술문이 바로 그것이다. |
Example 5.26. Output of romantest62.py against roman62.py
fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok fromRoman should fail with blank string ... okfromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 13 tests in 2.834s OK
![]() |
공백 문자열 테스트 사례는 이제 통과한다. 그래서 버그는 수정되었다. |
![]() |
다른 모든 테스트 사례들은 여전히 통과한다, 그것은 이 버그 수정이 다른 어떤 것도 망가뜨리지 않았음을 의미한다. 코딩을 중지하라. |
이런 방식으로 코딩한다고 해서 버그를 고치는 것이 더 쉽게 되지는 않는다. (이와 같이) 간단한 버그들은 간단한 테스트 사례를 요구한다; 복잡한 버그들은 복잡한 테스트 사례를 요구할 것이다. 테스트-중심 환경에서는, 버그를 수정하는 것이 시간이 많이 걸리는 듯이 보일지도 모른다. 왜냐하면 (테스트 사례를 작성하기 위하여) 그 버그가 무엇인지 코드로 조목조목 나열해야만 하고 버그 자체를 수정해야하기 때문이다. 그리고 나서 만약 테스트 사례가 즉각 통과하지 않는다면, 그 수정이 잘못되었는지 아니면 그 테스트 사례 자체가 버그를 가지고 있는지 알아 보아야만 한다. 그렇지만, 결국 이 테스트하는 코드와 테스트 받는 코드 사이의 앞으로-그리고-뒤로 전략은 그 자체로 이익이 있다. 왜냐하면 버그들이 한 번에 정확하게 수정될 가능성이 더 많도록 하여 주기 때문이다. 또한, 모든 테스트 사례를 새로운 사례로, 쉽게 재-실행할 수 있으므로 새로운 코드를 수정할 때 예전의 코드를 망가트릴 가능성이 훨씬 더 적게된다. 오늘의 유닛 테스트는 내일의 회귀 테스트이다.
고객을 땅 바닥에 쓰러뜨리고 가위 그리고 펄펄 끓는 왁스 같은 소름끼치게 무서운 것들로 고문하여 정확한 요구조건을 끌어내려는 최선의 노력에도 불구하고, 요구조건은 변경된다. 대부분의 고객들은 실제로 보기 전까지는 원하는 것을 알지 못한다. 그리고 알지라도, 원하는 것을 조목조목 정확하게 사용할 만한 정도로 나열하는 것에 익숙하지 못하다. 그리고 익숙하다고 할지라도, 다음 배포 때에는 어쨋든 더 많은 것을 원할 것이다. 그래서 요구조건이 변경됨에 맞추어 테스트 사례를 갱신할 수 있도록 준비하라.
예를 들어, 로마 숫자 변환 함수의 범위를 확장하기를 원한다고 가정해보라. 어떠한 문자도 세번을 초과하여 반복될 수 없다는 그 규칙을 기억하는가? 자, 그 로마인들은 기꺼이 그 규칙에 예외를 만들어서 4개의 M 문자를 일렬로 나열하여 4000을 표현하려고 하였다. 이렇게 변경을 하면, 변환가능한 숫자의 범위를 1..3999에서 1..4999으로 확장할 수 있을 것이다. 그러나 먼저 테스트 사례에 약간의 변경을 할 필요가 있다.
Example 5.27. Modifying test cases for new requirements (romantest71.py)
아직 그렇게 하지 못했다면, 이 예제와 이 책에서 사용된 다른 예제들을 내려 받을 수 있다 (Windows, UNIX, Mac OS).
import roman71 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'), (4000, 'MMMM'),(4500, 'MMMMD'), (4888, 'MMMMDCCCLXXXVIII'), (4999, 'MMMMCMXCIX')) def testToRomanKnownValues(self): """toRoman should give known result with known input""" for integer, numeral in self.knownValues: result = roman71.toRoman(integer) self.assertEqual(numeral, result) def testFromRomanKnownValues(self): """fromRoman should give known result with known input""" for integer, numeral in self.knownValues: result = roman71.fromRoman(numeral) self.assertEqual(integer, result) class ToRomanBadInput(unittest.TestCase): def testTooLarge(self): """toRoman should fail with large input""" self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 5000)
def testZero(self): """toRoman should fail with 0 input""" self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 0) def testNegative(self): """toRoman should fail with negative input""" self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, -1) def testDecimal(self): """toRoman should fail with non-integer input""" self.assertRaises(roman71.NotIntegerError, roman71.toRoman, 0.5) class FromRomanBadInput(unittest.TestCase): def testTooManyRepeatedNumerals(self): """fromRoman should fail with too many repeated numerals""" for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s) def testRepeatedPairs(self): """fromRoman should fail with repeated pairs of numerals""" for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): self.assertRaises(roman71.InvalidRomanNumeralError, roman71.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(roman71.InvalidRomanNumeralError, roman71.fromRoman, s) def testBlank(self): """fromRoman should fail with blank string""" self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, "") class SanityCheck(unittest.TestCase): def testSanity(self): """fromRoman(toRoman(n))==n for all n""" for integer in range(1, 5000):
numeral = roman71.toRoman(integer) result = roman71.fromRoman(numeral) self.assertEqual(integer, result) class CaseCheck(unittest.TestCase): def testToRomanCase(self): """toRoman should always return uppercase""" for integer in range(1, 5000): numeral = roman71.toRoman(integer) self.assertEqual(numeral, numeral.upper()) def testFromRomanCase(self): """fromRoman should only accept uppercase input""" for integer in range(1, 5000): numeral = roman71.toRoman(integer) roman71.fromRoman(numeral.upper()) self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, numeral.lower()) if __name__ == "__main__": unittest.main()
이제 우리의 테스트 사례는 새로운 요구조건으로 새로워졌다. 그러나 코드는 그렇지 않다. 그래서 테스트 사례중 약간은 실패하리라고 예상한다.
Example 5.28. Output of romantest71.py against roman71.py
fromRoman should only accept uppercase input ... ERRORtoRoman should always return uppercase ... ERROR fromRoman should fail with blank string ... ok fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ERROR
toRoman should give known result with known input ... ERROR
fromRoman(toRoman(n))==n for all n ... ERROR
toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok
====================================================================== ERROR: fromRoman should only accept uppercase input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 161, in testFromRomanCase numeral = roman71.toRoman(integer) File "roman71.py", line 28, in toRoman raise OutOfRangeError, "number out of range (must be 1..3999)" OutOfRangeError: number out of range (must be 1..3999) ====================================================================== ERROR: toRoman should always return uppercase ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 155, in testToRomanCase numeral = roman71.toRoman(integer) File "roman71.py", line 28, in toRoman raise OutOfRangeError, "number out of range (must be 1..3999)" OutOfRangeError: number out of range (must be 1..3999) ====================================================================== ERROR: fromRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 102, in testFromRomanKnownValues result = roman71.fromRoman(numeral) File "roman71.py", line 47, in fromRoman raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s InvalidRomanNumeralError: Invalid Roman numeral: MMMM ====================================================================== ERROR: toRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 96, in testToRomanKnownValues result = roman71.toRoman(integer) File "roman71.py", line 28, in toRoman raise OutOfRangeError, "number out of range (must be 1..3999)" OutOfRangeError: number out of range (must be 1..3999) ====================================================================== ERROR: fromRoman(toRoman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 147, in testSanity numeral = roman71.toRoman(integer) File "roman71.py", line 28, in toRoman raise OutOfRangeError, "number out of range (must be 1..3999)" OutOfRangeError: number out of range (must be 1..3999) ---------------------------------------------------------------------- Ran 13 tests in 2.213s FAILED (errors=5)
이제 새로운 요구조건때문에 실패하는 테스트 사례를 가지고 있으므로, 코드를 수정하여 테스트 사례와 조화시키는 것에 관하여 생각할 수 있다. (처음으로 유닛 테스트를 코딩하기 시작할 때 기억해야할 한 가지는 테스트 될 코드는 테스트 사례보다 절대로 “먼저(ahead)”가 아니라는 것이다. 코드가 여전히 뒤이기는 하지만, 여전히 뭔가를 작동시키고 있으며, 코드가 테스트 사례를 따라 잡는 순간, 코딩을 중지한다.)
Example 5.29. Coding the new requirements (roman72.py)
"""Convert to and from Roman numerals""" import re #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Define digit mapping romanNumeralMap = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) def toRoman(n): """convert integer to Roman numeral""" if not (0 < n < 5000):raise OutOfRangeError, "number out of range (must be 1..4999)" if int(n) <> n: raise NotIntegerError, "decimals can not be converted" result = "" for numeral, integer in romanNumeralMap: while n >= integer: result += numeral n -= integer return result #Define pattern to detect valid Roman numerals romanNumeralPattern = '^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'
def fromRoman(s): """convert Roman numeral to integer""" if not s: raise InvalidRomanNumeralError, 'Input can not be blank' if not re.search(romanNumeralPattern, s): raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) return result
이 두 개의 작은 변경이 필요한 모든 것이라는 것이 의심스러울 수 있다. 자, 내 말을 곧이 곧대로 믿지 마라; 여러분 스스로 알아보라:
Example 5.30. Output of romantest72.py against roman72.py
fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok fromRoman should fail with blank string ... ok fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 13 tests in 3.685s OK
![]() | 모든 테스트 사례는 통과한다. 코딩을 멈추어라. |
포괄적인 유닛 테스트가 뜻하는 것은 “나를 믿으시오”라고 말하는 프로그래머에게 의존할 필요가 없다는 것이다.
포괄적인 유닛 테스트에 관하여 가장 좋은 것은 모든 테스트 사례가 통과하였을 때 느끼는 감정이 아니며, 어떤 다른 사람이 그들의 코드를 망쳐놓았다고 여러분을 비난할 때에 그리고 실제로 그런 것이 아니라는 것을 여러분이 증명할 수 있었을 때 느끼는 그 감정도 아니다. 유닛 테스트에 관하여 가장 좋은 것은 냉혹하게 요소화할 수 있는 자유를 여러분에게 준다는 것이다.
요소화는 작동하고 있는 코드를 취해서, 그것을 더욱 잘 작동하도록 하는 처리과정이다. 보통, “더 좋은 (better)” 이라는 것은 “더 빠른 (faster)”을 의미하는데, 그렇지만 그것은 또한 “더 적은 메모리를 사용함”을 의미할 수도 있고, 또는 “더 적은 디스크 용량을 사용함”을 의미하거나, 혹은 단순하게 “더 우아하게”를 의미할 수 있다. 여러분에게, 프로젝트에, 개발환경에, 그것이 어떤 의미이든지 간에, 요소화는 모든 프로그램의 장기적인 건강에 중요하다.
여기에서 “더 좋은 (better)”는 “더 빠른 (faster)”을 의미한다. 구체적으로 말하면, fromRoman 함수는 실제로 필요한 속도 보다 느리다. 로마 숫자를 검증하기 위하여 사용한, 그 거대하고 성가신 정규 표현식 때문이다. 정규 표현식을 아예 없애 버리려고 노력하는 것은 아마도 가치가 없을 것 같다 (고단한 작업일 수도 있고, 성과가 더 빠르게 된다는 보장도 없다). 그러나 정규 표현식을 미리 편집함(precompile)으로써 함수의 속도를 높일 수 있다.
Example 5.31. Compiling regular expressions
>>> import re >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'M')<SRE_Match object at 01090490> >>> compiledPattern = re.compile(pattern)
>>> compiledPattern <SRE_Pattern object at 00F06E28> >>> dir(compiledPattern)
['findall', 'match', 'scanner', 'search', 'split', 'sub', 'subn'] >>> compiledPattern.search('M')
<SRE_Match object at 01104928>
![]() | |
한 번이상 정규 표현식을 사용하려고 할 때마다, 그것을 편집하여 패턴 객체를 획득하도록 하여야만 하고, 그리고 패턴 객체에 있는 메쏘드들을 직접적으로 호출하여야 한다. |
Example 5.32. Compiled regular expressions in roman81.py
아직 그렇게 하지 않았다면, 이 예제와 함께 이 책에서 사용된 다른 예제들을 내려 받을 수 없다 (Windows, UNIX, Mac OS).
# toRoman and rest of module omitted for clarity romanNumeralPattern = \ re.compile('^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$')def fromRoman(s): """convert Roman numeral to integer""" if not s: raise InvalidRomanNumeralError, 'Input can not be blank' if not romanNumeralPattern.search(s):
raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) return result
그래서 우리의 정규 표현식을 편집하면 얼마나 더 빠른가? 여러분 스스로 알아보라:
Example 5.33. Output of romantest81.py against roman81.py
.............---------------------------------------------------------------------- Ran 13 tests in 3.385s
OK
![]() | 여기의 건네주기에서 주의사항: 이 번에, 나는 그 유닛 테스트를 -v 선택사항 없이 실행했다, 그래서 각 테스트에 대한 완전한 문서화 문자열 (doc string) 대신에, 우리는 통과되는 각 테스트에 대하여 한 개의 점만을 얻는다. (만약 테스트가 실패하면, 우리는 F를 획득하게 될 것이다, 그리고 만약 그것이 에러를 가진다면, 우리는 E를 얻게 될 것이다. 우리는 각각의 실패와 에러에 대하여 여전히 완전한 역 추적을 유지할 것이며, 그래서 우리는 어떠한 문제도 추적해 내려갈 수가 있다.) |
![]() | 13개의 테스트를 정규 표현식을 미리 편집하지 않은 3.685초 와 비교하여, 3.385초 만에 실행했다. 전반적으로 8%가 개선되었다. 그리고 기억할 것은 유닛 테스트 동안에 소비되는 대부분의 시간은 다른 일들을 하는 데에 소비된다는 것이다. (따로 나는 정규 표현식을 유닛 테스트의 나머지와 떼어서, 그 자체로 시간 측정을 하였다. 그리고 내가 발견한 것은 이 정규 표현식을 편집하면 search를 평균적으로 54%정도 속도 증가시킨다는 것이었다.) 이런 간단한 수정치고는 좋은 결과이다. |
![]() | 오! 경탄해 마지 않을 경우로, 정규 표현식을 미리 편집하더라도 어떤 것도 파괴되지 않았고, 그것을 증명해 보였다. |
시도해보고 싶은 또 하나의 수행 최적화가 있다. 정규 표현식 구문의 복잡성을 고려하면, 같은 표현을 작성하는데 한 개 이상의 방법이 있다고 해도 놀랄만한 일은 아닐 것이다. comp.lang.python에서 이 모듈에 관한 약간의 토론후에, 어떤 사람의 제안을 받아들여서 나는 선택적인 반복된 문자열 대신에 {m,n} 구문을 사용하기를 시도하였다.
아직 그렇게 하지 못했다면, 이 예제와 더불어 이 책에서 사용된 다른 예제들을 내려 받을 수 있다 (Windows, UNIX, Mac OS).
# rest of program omitted for clarity #old version #romanNumeralPattern = \ # re.compile('^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$') #new version romanNumeralPattern = \ re.compile('^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$')![]()
![]() | M?M?M?M?을 M{0,4}로 대치하였다. 둘 다 같은 것을 의미한다: “0에서 4개 까지의 M 문자를 일치시킨다”. 비슷하게, C?C?C?는 C{0,3}이 되었다 (“0 에서 4 개까지의 C 문자를 일치시킨다”) 그리고 등등 X 그리고 I도 마찬가지다. |
정규 표현식의 이런 형태는 약간 더 짧다 (그렇지만 읽기는 쉽지 않다). 큰 문제는 얼마나 더 빠른가?
Example 5.35. Output of romantest82.py against roman82.py
............. ---------------------------------------------------------------------- Ran 13 tests in 3.315sOK
다른 하나만 더 비틀어 보고 싶다. 그리고 나서 요소화를 멈추고 이 모듈을 안정시킬 것이라고 약속한다. 반복해서 보았듯이, 정규 표현식은 대단히 난삽해질 수 있고 그리고 대단히 빠르게 판독 불가능해질 수 있다. 6개월이 지난후에 다시 이 모듈에 돌아와 보수하려고 노력하고 싶지는 않다. 물론, 테스트 사례는 통과한다. 그래서 그것이 작동한다는 것을 안다. 그러나 만약 내가 어떻게 그것이 작동하는지 이해하지 못한다면, 나는 새로운 사양을 추가할수도, 새로운 버그를 수정할수도, 그렇지 않으면 그것을 유지보수할 수도 없을 것이다. 문서화는 핵심적으로 중요하다. 그리고 파이썬은 정규 표현식을 자세하게 문서화하는 방법을 제공한다.
아직 그렇게 하지 못했다면, 이 예제와 함께 이 책에서 사용된 다른 예제들을 내려 받을 수 있다 (Windows, UNIX, Mac OS).
# rest of program omitted for clarity #old version #romanNumeralPattern = \ # re.compile('^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$') #new version romanNumeralPattern = re.compile(''' ^ # beginning of string M{0,4} # thousands - 0 to 4 M's (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's), # or 500-800 (D, followed by 0 to 3 C's) (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's), # or 50-80 (L, followed by 0 to 3 X's) (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's), # or 5-8 (V, followed by 0 to 3 I's) $ # end of string ''', re.VERBOSE)![]()
Example 5.37. Output of romantest83.py against roman83.py
............. ---------------------------------------------------------------------- Ran 13 tests in 3.315sOK
유닛 테스트는 강력한 개념으로, 적절히 구현되면 유지 비용을 줄일 수도 있으며 모든 장기 프로젝트에서 유연성을 증가시킬 수 있다. 또한 중요한 것은 유닛 테스트는 만병통치약도 신비스러운 문제 해결사도 또는 은빛 총알도 아니라는 것을 이해 하는 것이다. 훌륭한 테스트 사례를 작성하는 것은 어렵다. 그리고 최신으로 유지하는 것은 엄한 규율을 필요로 한다 (특히나 손님이 치명적인 버그 수정을 맹열히 요구할 때 그렇다). 유닛 테스트는 기능적인 테스트, 통합 테스트, 그리고 사용자 승인 테스트를 포함한, 다른 형태의 테스트의 대치물이 아니다. 그러나 유닛 테스트는 그럴 듯하다. 그리고 잘 작동한다. 그리고 그것이 작동하는 것을 보게 되면 여러분은 그것 없이 도데체 어떻게 지냈는지 놀라게 될 것이다.
이 장은 많은 기본을 다루었다. 그리고 심지어 많은 부분은 파이썬-특정적이지 않았다. 많은 언어에는 나름대로 유닛 테스트 작업틀이 있다. 그 모두가 똑 같이 다음과 같은 기본적 개념들을 이해하기를 요구한다:
또한, 편안하게 다음의 파이썬-특정의 것들을 수행할 수 있어야 한다:
더 읽어야 할 것
[11] “나는 모든 것을 거부할 수 있다, 유혹만 빼면 말이다.” --Oscar Wilde
제 4 장 HTML 처리 | 목 차 | 부 록 >> |