본문 바로가기

IT Book Summary/TDD

xUnit 21장 - 24장

21장. 셈하기

 

할일 목록

테스트메서드 호출하기

먼저 setUp 호출하기

나중에 tearDown 호출하기

테스트메서드가 실패하더라도 tearDown 호출하기

테스트 여러개 실행하기

수집한 결과를 출력하기

WasRun에 로그 문자열 남기기

 

테스트가 작동하도록 하려면 예외를 잡아야함.

기능을 구현하다 실수하면 예외가 보고되지 않으므로 실수를 알 방법이 없기 때문에.

 

일반적으로 테스트 구현 순서는 중요.

나에게 가르침을 주고 확신이 드는것을 선택한다.

 

우린 여러 테스트를 실행하고 그 결과를 보기를 원한다.

"5개 테스트가 실행됨, 2개 실패,

TestCaseTest.testFooBar-ZeroDivide Exception, MoneyTest.testNegarion-AssertionError"

 

그후 우리는 에러를 잡을 수 있다.

 

일단 TestCase.run()이 하나의 실행결과를 기록하는 TestResult객체를 반환하게 하자

 

TestCaseTest
def testResult(self):
    test= WasRun("testMethod")
    result= test.run()
    assert("1 run, 0 failed" == result.summary())
# 가짜 구현으로 시작
TestResult
class TestResult:
    def summary(self):
    return "1 run, 0 failed"
    
# 그리고 TestCase.run()이 TestResult를 반환
TestCase
def run(self):
    self.setUp()
    method = getattr(self, self.name)
    method()
    self.tearDown()
    return TestResult()

 

테스트가 실행됨.

 

이제 summary() 구현을 실체화

 

우선 실행된 테스트의 수를 상수화 

 

TestResult
    def __init__(self): self.runCount= 1
    def summary(self):
    return "%d run, 0 failed" % self.runCount
# %연산자는 sprintf와 같음.

 

runCount의 수치는 실제 실행된 테스트의 수를 계산해야 함.

0으로 초기화 하고 테스트 실행시 +1 

 

이후 새 메서드를 실제로 호출하자

 

TestResult
    def __init__(self): 
        self.runCount= 0
    def testStarted(self):
        self.runCount= self.runCount + 1
    def summary(self):
        return "%d run, 0 failed" % self.runCount
# 새 메서드를 실제로 호출
TestCase
def run(self):
    result= TestResult() 
    result.testStarted() 
    self.setUp()
    method = getattr(self, self.name)
    method()
    self.tearDown()
    return result

 

그리고 실패하는 테스트를 테스트하는 테스트를 하나 더 작성하자.

 

TestCaseTest
    def testFailedResult(self):
    test= WasRun("testBrokenMethod") 
    result= test.run()
    assert("1 run, 1 failed", result.summary())

# testBrokenMethod도 추가

WasRun
    def testBrokenMethod(self): 
    raise Exception

 

아직 예외 처리를 하지 않았다.

할일 목록에 남기자.

 

할일 목록

테스트메서드 호출하기

먼저 setUp 호출하기

나중에 tearDown 호출하기

테스트메서드가 실패하더라도 tearDown 호출하기

테스트 여러개 실행하기

수집한 결과를 출력하기

WasRun에 로그 문자열 남기기

실패한 테스트 보고하기

 

  • 가짜 구현을 한 뒤 상수를 변수로 바꾸어 실제 구현
  • 또 다른 테스트를 작성
  • 테스트 실패시, 좀더 작은 스케일로 또 다른 테스트를 만들어 실패한 테스트가 성공하게 보조함.

22장. 실패 처리하기

 

좀더 세밀한 테스트를 작성해 결과를 확인해보자

 

TestCaseTest
def testFailedResultFormatting(self): 
    result= TestResult() 
    result.testStarted() 
    result.testFailed()
    assert("1 run, 1 failed" == result.summary())

 

testStarted()와 testFailed() 는 각각 테스트를 시작할때, 실패할때 보낼 메세지임.

 

summary문자열이 제대로 출력된다면, 

어떻게 이 메시지를 보낼것인지 생각하자.

 

구현은 실패 횟수를 세는것

 

TestResult
def __init__(self): 
    self.runCount= 0 
    self.errorCount= 0
def testFailed(self):
    self.errorCount= self.errorCount + 1

 

횟수가 맞다면 출력이 될것이다.

 

TestResult
def summary(self):
    return "%d run, %d failed" % (self.runCount, self.failureCount)

 

이제 testFailed()만 호출하면 답을 얻을수 있음.

언제 호출해야 할까?

-> 테스트 메서드에서 던진 예외를 잡았을 때

 

TestCase
def run(self):
    result= TestResult() 
    result.testStarted() 
    self.setUp()
    try:
        method = getattr(self, self.name)
        method()
    except:
        result.testFailed() 
    self.tearDown()
    return result

 

setUp()에서 문제가 발생시 예외가 잡히지 않음.(할일목록에 추가)

 

우리는 테스트가 독립적으로 실행되길 원함.

 

코드를 수정하기 위해서는 또 다른 테스트가 필요함.

 

항상 테스트를 먼저 작성해야 한다는 사실을 의식적으로 상기하자.

 

  • 작은 스케일의 테스트가 통과하게 만들었다.
  • 큰 스케일의 테스트를 다시 도입
  • 작은 스케일의 테스트에서 보았던 매커니즐을 이용해 큰 스케일의 테스트를 빠르게 통과
  • 중요한 문제를 발견했는데 바로 처리하지 않고 할일 목록에 적어둠.

 


23장. 얼마나 달콤한지 How Suite It Is

 

테스트들을 호출하는 코드가 지저분해 보인다.

 

중복은 나쁜것.

 

TestSuite를 구현해야하는 장점은 컴포지트 패턴의 순수한 예제를 제공할 수 있다는 점.

 

테스트 하나와 테스트 집단을 동일하게 다루고 싶은 것

 

TestSuite를 만들고 거기에 테스트를 여러개 넣고 모두 실행해 결과를 얻어내자.

 

TestCaseTest
    def testSuite(self):
    suite= TestSuite() 
    suite.add(WasRun("testMethod")) 
    suite.add(WasRun("testBrokenMethod")) 
    result= suite.run()
    assert("2 run, 1 failed" == result.summary())

# add()메서드를 구현하는것은 테스트들을 리스트에 추가하는 작업.

TestSuite
class TestSuite:
    def __init__(self):
        self.tests= [] 
    def add(self, test):
        self.tests.append(test)
# []는 빈컬렉션 생성
# 하나의 TestResult에 모든 테스트에 대해 쓰이게 반복루프
    def run(self):
        result= TestResult() 
        for test in tests:
            test.run(result) 
        return result

 

컴포지트의 주요 제약 중 하나는 컬렉션이 하나의 개별 아이템인 것처럼 반응해야 한다는 것.

TestCase.run()에 매개변수를 추가하게 되면 TestSuite.run()에도 같은 매개변수를 추가해야 함.

 

떠오른 세가지 대안

  • 파이썬의 기본 매개 변수 기능을 사용. 불행히 기본값은 컴파일타임에 평가되므로 하나의 TestResult를 재사용할 수 없음.
  • 메서드를 두 부분으로 나눔. 하나는 TestResult를 할당하는 부분, 할당된 TestResult를 가지고 테스트를 수행하는 부분.
  • 호출하는 곳에서 TestResults를 할당.

호출하는 곳에서 TestResult를 할당하는 전략을 선택하자

매개변수 수집 collecting parameter 패턴 

- 이 해법은 run()이 명시적으로 반환하지 않아도 된다는 추가적인 장점

 

TestCaseTest
def testSuite(self):
    suite= TestSuite() 
    suite.add(WasRun("testMethod")) 
    suite.add(WasRun("testBrokenMethod")) 
    result= TestResult()
    suite.run(result)
    assert("2 run, 1 failed" == result.summary())

TestSuite
def run(self, result): 
    for test in tests:
        test.run(result)
        
TestCase
def run(self, result): 
    result.testStarted() 
    self.setUp()
    try:
        method = getattr(self, self.name)
        method()
    except:
        result.testFailed() 
    self.tearDown()

 

이제 테스트 호출 코드를 정리하자

 

suite= TestSuite() 
suite.add(TestCaseTest("testTemplateMethod")) 
suite.add(TestCaseTest("testResult")) 
suite.add(TestCaseTest("testFailedResultFormatting")) 
suite.add(TestCaseTest("testFailedResult")) 
suite.add(TestCaseTest("testSuite"))
result= TestResult()
suite.run(result)
print result.summary()

 

중복이 많다. 

우선 실패하는 테스트 세개를 고쳐야 함.

(기존의 인자없이 run인터페이스를 사용하는 테스트)

 

TestCaseTest
def testTemplateMethod(self):
    test= WasRun("testMethod")
    result= TestResult()
    test.run(result)
    assert("setUp testMethod tearDown " == test.log)
def testResult(self):
    test= WasRun("testMethod")
    result= TestResult()
    test.run(result)
    assert("1 run, 0 failed" == result.summary())
def testFailedResult(self):
    test= WasRun("testBrokenMethod")
    result= TestResult()
    test.run(result)
    assert("1 run, 1 failed" == result.summary())
def testFailedResultFormatting(self): 
    result= TestResult() 
    result.testStarted() 
    result.testFailed()
    assert("1 run, 1 failed" == result.summary())

 

각 테스트들이 TestResult를 할당하고 있음을 주목.

 

TestResult를 setUp()에서 생성하게 만들어 테스트를 단순화

처음의 wasRun객체생성을 setUp메서드에서 하게 만들었던것을 기억한다.

 

TestCaseTest
def setUp(self):
    self.result= TestResult()
def testTemplateMethod(self):
    test= WasRun("testMethod")
    test.run(self.result)
    assert("setUp testMethod tearDown " == test.log)
def testResult(self):
    test= WasRun("testMethod")
    test.run(self.result)
    assert("1 run, 0 failed" == self.result.summary())
def testFailedResult(self):
    test= WasRun("testBrokenMethod") 
    test.run(self.result)
    assert("1 run, 1 failed" == self.result.summary())
def testFailedResultFormatting(self): 
    self.result.testStarted()
    self.result.testFailed()
    assert("1 run, 1 failed" == self.result.summary())
def testSuite(self):
    suite= TestSuite() 
    suite.add(WasRun("testMethod")) 
    suite.add(WasRun("testBrokenMethod")) 
    suite.run(self.result)
    assert("2 run, 1 failed" == self.result.summary())

 

파이썬이 객체지원이 추가된 스크립트 언어다보니

전역참조가 암묵적으로 이루어지고 self에 대한 참조를 명시적으로 적어야 함.

 

할일 목록

테스트메서드 호출하기

먼저 setUp 호출하기

나중에 tearDown 호출하기

테스트메서드가 실패하더라도 tearDown 호출하기

테스트 여러개 실행하기

수집한 결과를 출력하기

WasRun에 로그 문자열 남기기

실패한 테스트 보고하기

setUp 에러를 잡아서 보고하기

TestCase 클래스에서 TestSuite 생성하기

 

이 목록의 나머지 항목은 직접 TDD 해볼수 있게 남겨둠.

 

  • TestSuite를 위한 테스트를 작성
  • 테스트를 통과시키지 못한 채 일부분만 구현. '규칙'을 위반.테스트를 통과 시키고 리팩토링할 가짜구현이 떠오르지 않음
  • 아이템과 아이템의 모음(컴포지트)이 동일하게 작동할 수 있도록 run메서드의 인터페이스를 변경. 테스트 통과.
  • 공통된 셋업 코드를 분리.

24장. xUnit 회고

 

세세한 구현사항 보다 우리가 사용한 테스트 케이스가 더 중요하다.

 

이 책에서 보여준 테스트케이스를 지원할 수 있다면

독립적이고 여러 테스트를 조합할 수 있는 테스트를 작성할 수 있을것이고,

테스트 우선으로 개발할 준비가 될 것.

 

xUnit을 직접 구현해 볼만한 두가지 이유.

  • 숙달: xUnit의 정신은 간결함. 직접 만들어 사용하면 숙달된 도구를 쓰는 느낌을 받을 것이다.
  • 탐험: 새로운 프로그래밍 언어를 접하면 그 언어로 xUnit을 만들어보자. 테스트가 열개정도 통과하라 때쯤이면 그 언어로 프로그래밍하면서 접하게 될 많은 기능을 한 번씩 경험해보게 된다.

단언 assertion 의 실패와 나머지 종류의 에러 사이에 큰 차이점이 있음.

단언 실패가 디버깅 시간이 더 걸림.

때문에 xUnit 구현에서는 단언 실패와 에러를 구별함.

GUI에서는 종종 에러를 상단에 출력하는 식으로 이들을 구분.

 

JUnit은 Test 인터페이스를 선언하는데 TestCase와 TestSuite 모두 이를 상속함.

만약 JUnit 도구가 테스트를 실행하게 만들고 싶으면 Test 인터페이스를 구현하면 됨.

 

public interface Test {
    public abstract int countTestCases();
    public abstract void run(TestResult result);
}

 

동적 타이핑 언어에서는 그냥 해당 오퍼레이션을 구현하면 됨.

스크립트 언어의 경우,

스크립트에서 countTestCases()가 1을 반환하도록 구현하고,

실패시 TestResult에 고지하도록 하면, 

그 스크립트를 일반 TestCase 와 함께 실행 가능.

 

 

 

 

 

 

 

 

'IT Book Summary > TDD' 카테고리의 다른 글

29장 - 31장  (0) 2020.04.12
3부 테스트 주도 개발의 패턴. 25장- 28장  (0) 2020.04.07
18장-20장 xUnit  (0) 2020.03.24
13-17장  (0) 2020.03.18
10-12장  (0) 2020.03.10