이번 장에서는 테스트 주도 개발을 위한 도구의 구현에 대해 얘기해보려 한다. 당연히 테스트 주도로.
우선 테스트 케이스를 만들고 테스트 메서드를 실행할 수 있어야 한다. 이때 테스트 케이스를 작성하기 위해 사용할 프레임워크를 테스트하기 위한 테스트 케이스를 작성해야 하는... 문제가 있다. 아직 프레임워크가 없기 때문에 첫 번째 작은 단계는 수동으로 검증을 할 것이다.
- 테스트 메서드 호출하기
- 먼저 setUp 호출하기
- 나중에 tearDown 호출하기
- 테스트 메서드가 실패하더라도 tearDown 호출하기
- 여러 개의 테스트 실행하기
- 수집된 결과를 출력하기
첫 번째 원시 테스트에는 테스트 메서드가 호출되면 true, 그렇지 않으면 false를 반환할 작은 프로그램이 필요하다. 테스트 메서드 안에 플래그를 설정하는 테스트 케이스가 있다면, 그 테스트 케이스가 실행된 이후에 플래그를 인쇄해 볼 수 있고, 그러면 그게 맞는지 아닌지 확인해 볼 수 있다.
메서드가 실행되었는지를 알려주는 테스트 케이스이므로 클래스 이름을 WasRun으로 하고, 플래그 역시 wasRun으로 하자.
test = WasRun("testMethod")
print(test.wasRun)
test.testMethod()
print(test.wasRun)
class WasRun:
def __init__(self, name):
self.wasRun = None
def testMethod(self):
self.wasRun = 1
다음으로 필요한 것은 테스트 메서드를 직접 호출하는 대신 진짜 인터페이스인 run() 메서드를 사용하는 것이다.
test = WasRun("testMethod")
print(test.wasRun)
test.run()
print(test.wasRun)
class WasRun:
# ...
def run(self):
self.testMethod()
이번에는 testMethod()를 동적으로 호출하도록 바꿔보자. 파이썬은 클래스의 이름이나 메서드의 이름을 함수처럼 다룰 수 있으므로 이를 이용해서 객체를 얻어낼 수 있다.
class WasRun:
def __init__(self, name):
self.wasRun = None
self.name = name
def testMethod(self):
self.wasRun = 1
def run(self):
method = getattr(self, self.name)
method()
이제 WasRun 클래스는 독립된 두 가지 일을 수행한다. 하나는 메서드가 호출되었는지 그렇지 않은지를 기억하는 일이고, 다른 하나는 메서드를 동적으로 호출하는 일이다. 이제 본격적으로 테스트 케이스를 만들어보자.
class TestCase:
def __init__(self, name):
self.name = name
def run(self):
method = getattr(self, self.name)
method()
class WasRun(TestCase):
def __init__(self, name):
self.wasRun = None
TestCase.__init__(self, name)
def testMethod(self):
self.wasRun = 1
매번 None이랑 1이 나오는지 확인하는 것도 자동화하자.
class TestCaseTest(TestCase):
def testRunning(self):
test = WasRun("testMethod")
assert not test.wasRun
test.run()
assert test.wasRun
TestCaseTest("testRunning").run()
- 테스트 메서드 호출하기
- 먼저 setUp 호출하기
- 나중에 tearDown 호출하기
- 테스트 메서드가 실패하더라도 tearDown 호출하기
- 여러 개의 테스트 실행하기
- 수집된 결과를 출력하기
테스트를 작성하다 보면 공통된 패턴을 발견하게 된다.
두 번째와 세 번째 단계인 행동과 확인 단계는 항상 다르지만, 처음 단계인 준비 단계는 여러 테스트에 걸쳐 동일한 경우가 종종 있다. 이 부분을 테스트로 표현해보자. TestCaseTest에서는 setUp이 되었는지 확인하는 코드가, WasRun에서는 실제로 wasSetUp을 1로 할당하는 코드가, TestCase에서는 setUp을 호출하는 코드가 필요할 것이다.
class TestCaseTest(TestCase):
# ...
def testSetUp(self):
test = WasRun("testMethod")
test.run()
assert test.wasSetUp
class WasRun(TestCase):
# ...
def __init__(self, name):
TestCase.__init__(self, name)
def setUp(self):
self.wasRun = None
self.wasSetUp = 1
class TestCase:
# ...
def setUp(self):
pass
def run(self):
self.setUp()
method = getattr(self, self.name)
method()
이렇게 바꾸면, 테스트를 실행하기 전에 플래그를 검사하지 않도록 testRunning을 단순화해야 한다. 더불어 테스트 자체도 단순화할 수 있다. testSetUp과 testRunning 두 경우 모두 WasRun의 인스턴스를 생성하는데, 그 대신 WasRun을 setUp에서 생성하고 테스트 메서드에서 그걸 사용하게 할 수 있다.
class TestCaseTest(TestCase):
def setUp(self):
self.test = WasRun("testMethod")
def testSetUp(self):
self.test.run()
assert self.test.wasSetUp
def testRunning(self):
self.test.run()
assert self.test.wasRun
- 테스트 메서드 호출하기
- 먼저 setUp 호출하기
- 나중에 tearDown 호출하기
- 테스트 메서드가 실패하더라도 tearDown 호출하기
- 여러 개의 테스트 실행하기
- 수집된 결과를 출력하기
언제까지 계속 플래그를 통해 실행 여부를 확인할 수는 없으므로 로그를 간단히 남기는 방식으로 전략을 바꾸자. 항상 로그의 끝부분에만 기록을 추가하면 메서드 호출 순서를 알 수 있을 것이다.
- 테스트 메서드 호출하기
- 먼저 setUp 호출하기
- 나중에 tearDown 호출하기
- 테스트 메서드가 실패하더라도 tearDown 호출하기
- 여러 개의 테스트 실행하기
- 수집된 결과를 출력하기
- WasRun에 로그 문자열 남기기
class WasRun(TestCase):
# ...
def setUp(self):
self.log = "setUp "
def testMethod(self):
self.log = self.log + "testMethod "
class TestCaseTest(TestCase):
# ...
def testSetUp(self):
self.test.run()
assert "setUp testMethod" == self.test.log
이제 testSetUp 테스트는 두 개의 테스트가 할 일을 모두 수행하므로, testRunning을 지우고 testSetUp의 이름을 알맞게 바꿔주자.
class TestCaseTest(TestCase):
def setUp(self):
pass
def testTemplateMethod(self):
test = WasRun("testMethod")
test.run()
assert "setUp testMethod" == test.log
- 테스트 메서드 호출하기
- 먼저 setUp 호출하기
- 나중에 tearDown 호출하기
- 테스트 메서드가 실패하더라도 tearDown 호출하기
- 여러 개의 테스트 실행하기
- 수집된 결과를 출력하기
- WasRun에 로그 문자열 남기기
이제 tearDown 구현을 위한 테스트를 작성한 후 구현해보자.
class TestCaseTest(TestCase):
# ...
def testTemplateMethod(self):
test = WasRun("testMethod")
test.run()
assert "setUp testMethod tearDown " == test.log
class TestCase:
# ...
def run(self):
self.setUp()
method = getattr(self, self.name)
method()
self.tearDown()
def tearDown(self):
pass
class WasRun(TestCase):
# ...
def tearDown(self):
self.log = self.log + "tearDown "
- 테스트 메서드 호출하기
- 먼저 setUp 호출하기
- 나중에 tearDown 호출하기
- 테스트 메서드가 실패하더라도 tearDown 호출하기
- 여러 개의 테스트 실행하기
- 수집된 결과를 출력하기
- WasRun에 로그 문자열 남기기
tearDown()은 테스트 메서드에서 예외가 발생하건 말건 호출되도록 보장되어야 한다. 하지만 테스트가 작동하도록 하려면 예외를 잡아야 한다. 즉, 여러 테스트를 실행했을 때 "5개 테스트가 실행됨. 2개 실패."과 같은 결과를 보길 원한다. 이를 위해 TestCase.run()이 테스트 하나의 실행 결과를 기록하는 TestResult 객체를 반환하게 만들자.
class TestCaseTest(TestCase):
# ...
def testResult(self):
test = WasRun("testMethod")
result = test.run()
assert "1 run, 0 failed" == result.summary()
우선 가짜 구현으로 테스트를 통과하게끔 작성하자.
class TestResult:
def summary(self):
return "1 run, 0 failed"
class TestCase:
# ...
def run(self):
self.setUp()
method = getattr(self, self.name)
method()
self.tearDown()
return TestResult
이제 summary() 구현을 조금씩 실체화하자.
class 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
class TestCase:
# ...
def run(self):
result = TestResult()
result.testStarted()
self.setUp()
method = getattr(self, self.name)
method()
self.tearDown()
return result
실패하는 테스트의 수도 비슷한 흐름으로 작성하자. 먼저 테스트 케이스부터.
class TestCaseTest(TestCase):
# ...
def testFailedResult(self):
test = WasRun("testBrokenMethod")
result = test.run()
assert "1 run, 1 failed" == result.summary()
class WasRun(TestCase):
# ...
def testBrokenMethod(self):
raise Exception
지금은 WasRun.testBrokenMethod에서 던진 예외를 처리하지 않지만, 여기까지만 구현하고 할일 목록을 체크해보자.
- 테스트 메서드 호출하기
- 먼저 setUp 호출하기
- 나중에 tearDown 호출하기
- 테스트 메서드가 실패하더라도 tearDown 호출하기
- 여러 개의 테스트 실행하기
- 수집된 결과를 출력하기
- WasRun에 로그 문자열 남기기
- 실패한 테스트 보고하기
실패한 테스트를 발견하면 좀 더 세밀한 단위의 테스트를 작성해서 올바른 결과를 출력하는 걸 확인해보자.
class TestCaseTest(TestCase):
# ...
def testRailedResultFormatting(self):
result = TestResult()
result.testStarted()
result.testFailed()
assert "1 run, 1 failed" == result.summary()
class TestResult:
def __init__(self):
self.runCount = 0
self.failureCount = 0
def testStarted(self):
self.runCount = self.runCount + 1
def testFailed(self):
self.failureCount = self.failureCount + 1
def summary(self):
return "%d run, %d failed" % (self.runCount, self.failureCount)
class TestCase:
# ...
def run(self):
result = TestResult()
result.testStarted()
self.setUp()
try:
method = getattr(self, self.name)
method()
except:
result.testFailed()
self.tearDown()
return result
물론 위 run() 메서드에는 setUp()에서 발생한 예외에 대한 처리를 하지 못한다는 문제가 있다. 이는 추후 연습 과제로 남겨두자.
- 테스트 메서드 호출하기
- 먼저 setUp 호출하기
- 나중에 tearDown 호출하기
- 테스트 메서드가 실패하더라도 tearDown 호출하기
- 여러 개의 테스트 실행하기
- 수집된 결과를 출력하기
- WasRun에 로그 문자열 남기기
- 실패한 테스트 보고하기
이번에는 여러 테스트가 같이 실행될 수 있도록 만드는 일을 해보자. 현재 코드 파일의 끝 부분에는 모든 테스트들을 호출하는 코드가 있는데, 놓친 디자인 요소를 찾기 위해 일부러 만드는 중복이 아니라면 제거하는 편이 좋다.
TestCaseTest("testTemplateMethod").run().summary()
TestCaseTest("testResult").run().summary()
TestCaseTest("testFailedResult").run().summary()
TestCaseTest("testRailedResultFormatting").run().summary()
이들을 모아서 한 번에 실행할 수 있는 TestSuite를 만들고, 거기에 테스트를 몇 개 넣은 다음 모두 실행하여 결과를 얻어내 보자.
class TestCaseTest(TestCase):
# ...
def testSuite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.add(WasRun("testBrokenMethod"))
result = suite.run()
assert "2 run, 1 failed" == result.summary()
class TestSuite:
def __init__(self):
self.tests = []
def add(self, test):
self.tests.append(test)
def run(self):
result = TestResult()
for test in self.tests:
test.run(result)
return result
test.run()에 TestResult를 매개 변수로 넘겨주었기 때문에 호출하는 곳에서 TestResult를 할당하자.
class TestCaseTest(TestCase):
# ...
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()
class TestSuite:
# ...
def run(self, result):
for test in self.tests:
test.run(result)
return result
class 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("testFailedResult"))
suite.add(TestCaseTest("testRailedResultFormatting"))
suite.add(TestCaseTest("testSuite"))
result = TestResult()
suite.run(result)
print(result.summary())
그리고 이전에 작성했던 테스트 코드들은 run() 메서드에 TestResult를 넘겨주지 않았으므로 이를 알맞게 수정하자.
class TestCaseTest(TestCase):
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" == result.summary()
def testFailedResult(self):
test = WasRun("testBrokenMethod")
test.run(self.result)
assert "1 run, 1 failed" == result.summary()
def testRailedResultFormatting(self):
self.result.testStarted()
self.result.testFailed()
assert "1 run, 1 failed" == result.summary()
def testSuite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.add(WasRun("testBrokenMethod"))
suite.run(self.result)
assert "2 run, 1 failed" == result.summary()
- 테스트 메서드 호출하기
- 먼저 setUp 호출하기
- 나중에 tearDown 호출하기
- 테스트 메서드가 실패하더라도 tearDown 호출하기
- 여러 개의 테스트 실행하기
- 수집된 결과를 출력하기
- WasRun에 로그 문자열 남기기
- 실패한 테스트 보고하기
- TestCase 클래스에서 TestSuite 생성하기