TDD(Test-Driven-Development) By Example - 2부

Jeongmin Yeo (Ethan)·2021년 4월 11일
3
post-thumbnail

TDD(Test-Driven-Development) 2부. xUnit에 대해 정리합니다.

학습할 내용은 다음과 같습니다.

  • Intro
  • 18장. xUnit으로 가는 첫 걸음
  • 19장. 테이블 처리기
  • 20장. 뒷정리하기
  • 21장. 셈하기
  • 22장. 실패 처리하기
  • 23장. 얼마나 달콤한지
  • 24장. xUnit 회고

Reference: Test-Driven Development: By Example

Intro

테스트 주도 개발을 위한 도구의 구현에 대해 어떤 식으로 이야기 해야 할까? 당연하게도 테스트 주도로.

xUnit 아키텍처는 파이썬에서 아주 매끄럽게 도출되기 때문에, 2부에서는 언어를 파이썬으로 바꾸겠다.

너무 걱정하지는 말자. 파이썬을 실제로 접한 적이 없는 사람을 위해 간략히 설명 할테니까.

2부를 끝내고 나면, 파이썬에 대해 개론적으로 이해하고 자신만의 테스트 프레임워크를 작성할 수 있게 될 것이며 좀 더 교묘한 TDD의 예를 알게 될 것이다.


18장. xUnit으로 가는 첫 걸음

2부는 테스트 주도 개발을 통해 '실제' 소프트웨어를 만드는 발전된 예제로 생각하면 된다. 또 2부를 자기 참조(self-referential) 프로그래밍에 대한 전산학 실습으로 생각할 수 있다.

우선 테스트 케이스를 만들고 테스트 메소드를 실행할 수 있어야 한다.

예를 들면 Testcase("testMethod").run() 그런데 한 가지 부트스트랩의 문제가 있다. 우린 테스트 케이스를 작성하기 위해 사용할 프레임워크를 테스트 하기 위한 테스트 케이스를 작성해야 한다는 것이다.

아직 프레임워크가 없기 때문에 첫 번째 작은 단계는 수동적으로 검증해야 할 것이다.

우리는 이전에 했듯이 모든 것을 검증하면서 작은 단계를 밟아 나갈 것이다. 다음은 머릿속에 떠오른 테스트 프레임워크에 대한 할 일 목록이다.

테스트 메소드 호출하기

먼저 setUp 호출하기 

나중에 tearDown 호출하기

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

여러 개의 테스트 실행하기

수집된 결과를 출력하기

물론 이번에도 테스트를 먼저 만들 것이다.

우리의 첫 번째 원시 테스트에는 테스트 메소드가 호출되면 true 그렇지 않으면 false 를 반환하는 프로그램이 필요하다.

테스트 메소드 안에 플래그를 설정하는 테스트 케이스가 있다면, 그 테스트 케이스가 실행된 이후에 플래그를 인쇄해 볼 수 있고, 그러면 그게 맞는지 아닌지 확인해 볼 수 있다.

부트스트랩 테스트를 위한 전략이 있다. 플래그를 가지고 있는 테스트 케이스를 만들 것이다. 테스트 메소드가 실행되기 전에는 플래그가 false 인 상태여야 하고 테스트 메소드가 플래그를 설정할 것이고, 테스트 메소드가 실행된 후에는 플래그가 true 여야 한다.

메소드가 실행되었는지 알려주는 테스트 케이스이므로 클래스 이름을 WasRun 이라 하자. 플래그 역시 WasRun으로 하겠다.

파이썬은 파일에서 읽는 순서대로 명령문을 실행하기 때문에 테스트 메소드를 수동으로 호출하는 식으로 시작할 수 있다.

test = WasRun("testMethod")
print test.wasRun
test.testMethod()
print test.wasRun

우리가 예상하는 이 코드는 테스트 메소드가 실행하기 전에 None 을 호출하고 (파이썬에서 None 이란 null 과 유사하고 0 이나 다른 몇몇 객체와 함께 논리 값 false 를 의미한다.)

그 후에 1 을 출력할 것으로 예상된다.

하지만 아직 우리는 WasRun 클래스를 정의하지 않았으므로 예상대로 실행되지는 않는다.

# WasRun Class

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

구현 코드는 다음과 같다.

# WasRun Class

class WasRun:
  def __init__(self, name):
    	self.wasRun = None
  
  def testMethod(self):
    	self.wasRun=1
  
  def run(self):
    	self.testMethod()

다음 단계는 testMethod 를 동적으로 호출하는 것이다. 파이썬의 가장 멋진 특성 중 하나는 클래스의 이름이나 메소드의 이름을 함수처럼 다룰 수 있다는 점이다.

테스트 케이스의 이름과 같은 문자열을 갖는 필드가 주어지면 함수로 호출될 때 해당 메소드를 호출하게끔 하는 객체를 얻어낼 수 있다.

# WasRun Class

class WasRun:
  def __init__(self, name):
    	self.wasRun = None
      self.name = name
  
  def run(self):
    	method = getattr(self, self.name)
      method(); 

여기 리팩토링의 일반적인 패턴이 있다. 하나의 특별한 사례에 대해서만 작동하는 코드를 다른 여러 사례에 대해서도 동작할 수 있도록 상수를 변수로 일반화 시키는 것이다.

우리의 경우에 상수는 데이터 값이 아니라 하드 코딩된 코드지만 이 경우에도 원리는 같다.

이제 우리의 작은 WasRun 클래스는 독립된 두 가지 일을 한다. 하나는 메소드가 호출되었는지 그렇지 않은지를 기억하는 일이고, 또 다른 하나는 메소드를 동적으로 호출하는 일이다.

이제 TestCase 상위 클래스를 만들고 WasRun 이 이를 상속받도록 하자.

# TestCase class

class TestCase:
  def __init__(self, name):
    self.name = name
	
  def run(self):
    method = getattr(self, self.name)
    method();

# WasRun Class

class WasRun(TestCase):
  def __init__(self, name):
    self.wasRun = None
    TestCase.__init__(self,name)

리팩토링 후 잘 동작하는 지 확인한다.

매번 None 이랑 1 이 나오는지 확인하는 것도 지겹기 때문에 우리가 만든 코드를 다음과 같이 바꿀 수 있겠다.

# TestCaseTest class

class TestCaseTest(TestCase):
  def testRunning(self):
    test = WasRun("testMethod")
    assert(not test.wasRun)
    test.run()
    assert(test.wasRun)

TestCaseTest("testRunning").run()

테스트 코드의 내용을 단순히 print 문이 assert 문으로 바뀐 것이다.

다음 장에서는 setUp 을 호출하는 부분을 다룰 것이다. 그 전에 지금까지 한 것을 검토해보자.

  • 일단 하드코딩을 한 다음에 상수를 변수로 대체하여 일반성을 이끌어 내는 방식으로 기능을 구현했다.
  • 테스트 프레임워크를 작은 단계로만 부트스트랩했다.

19장. 테이블 처리기

테스트를 작성하다보면 공통된 패턴을 발견하게 될 것이다. (Bill Wake는 이 패턴을 3A라 부른다.)

  1. 준비(arrange) - 객체를 생성한다.
  2. 행동(act) - 어떤 자극을 준다.
  3. 확인(assert) - 결과를 검사한다.
먼저 setUp 호출하기 

두 번째와 세 번째 단계인 행동과 확인 단계는 항상 다르지만 처음 단계인 준비 단계는 여러 테스트에 걸쳐 동일한 경우가 종종 있다.

예를 들어 객체 7과 9가 있다고 해보자 첫번째 테스트에서는 더하기로 16이 나와야 하고 두번째 테스트에서는 곱하기로 63이 나와야 한다. 여기서 객체 7과 9는 항상 반복된다.

우리는 이 패턴에서 테스트를 위해 새로운 객체를 얼마나 자주 생성해야 하는가 하는 문제에 대해 직면하게 되는데 이 때 다음 두 가지 제약이 상충된다.

  • 성능 - 우린 테스트가 될 수 있는 한 빨리 실행되길 원한다. 여러 테스트에서 같은 객체를 사용한다면 객체 하나만 생성해서 모든 테스트가 이 객체를 쓰게 할 수 있을 것이다.
  • 격리 - 우린 한 테스트에서의 성공이나 실패가 다른 테스트에 영향을 주지 않기를 원한다. 만약 테스트들이 객체를 공유하는 상태에서 하나의 테스트가 공유 객체의 상태를 변경한다면 다음 테스트의 결과에 영향을 미칠 가능성이 있다.

테스트 사이의 커플링은 확실히 지저분한 결과를 야기한다.

한 테스트가 깨지면 다음 열 개의 테스트 코드가 올바르더라도 같이 깨지는 식이다. 또한 드물지만 매우 어려운 문제가 야기될 수 있다.

테스트가 실행되는 순서가 중요한 경우가 있는데, 만약 테스트 B를 실행하기 전에 테스트 A를 실행하면 둘 다 제대로 작동하지만 A를 실행하기 전에 B를 실행하면 A가 실패한다든지 이런 문제 상황이 나올 수 있다.

그러므로 테스트 커플링을 만들지 말자. 지금 일단 객체 생성을 충분히 빠르게 할 수 있다고 가정해보자. 이럴 경우 테스트가 돌 때마다 객체가 생성되길 원한다.

이것을 흉내낸 형태를 이전에 WasRun 에서 봤는데 거기에선 우리가 테스트를 실행하기 전에 플래그를 거짓으로 두었다.

이것을 이용해 한 걸음 진전시키자.

# TestCaseTest class

class TestCaseTest(TestCase):
  ... 
  def testSetUp(self):
    test = WasRun("testMethod")
    test.run()
    assert(test.wasSetUp)

파이썬이 wasSetUp 속성이 없다고 알려주므로 테스트틀 성공시키기 위해 수정하자.

# WasRun class
def setUp(self):
  self.wasSetUp = 1

이제 이 메소드를 호출하면 wasSetUp 이 설정될 것이다. setUp 을 호출하는 것은 TestCase 가 할 일이니 그곳을 보자.

# TestCase class
def setUp(self):
  pass

def run(self):
  self.setUp()
  method = getattr(self, self.name)
  method()

테스트 케이스 하나를 돌아가기 위해서는 두 단계가 필요하다. 이렇게 까다로운 상황에서는 너무 많은 단계이다. 한번에 메소드를 하나 이상 수정하지 않으면서 테스트가 통과하게 만들 수 있는 방법을 찾아내려고 노력해보자.

방금 만든 기능을 적용해서 우리 테스트를 짧게 줄일 수 있다. 우선 wasRun 플래그를 setUp 에서 설정하도록 하면 WasRun 을 단순화 할 수 있다.

# WasRun class
def setUp(self):
  self.wasRun = None
  self.wasSetUp = 1

테스트를 실행하기 전에 플래그를 검사하지 않도록 testRunning 을 단순화해야 한다.

# TestCaseTest class
def testRunning(self):
  test = WasRun("testMethod")
  test.run()
  assert(test.wasRun)

테스트 자체도 단순화 할 수 있다. 두 경우 모두 WasRun 인스턴스를 생성하는데 이를 setUp 에서 생성하고 테스트 메소드에서 그걸 사용하도록 할 수 있다.

# TestCaseTest class
def setUp(self)
	self.test = WasRun("testMethod")

def testRunning(self):
  self.test.run()
  assert(self.test.wasRun)

def testSetUp(self):
  self.test.run()
  assert(self.test.wasSetUp)

다음 장에서는 테스트 메소드가 실행된 후에 호출될 tearDown() 을 구쳔할 것이다. 이번 장에서 한 일을 검토해보자.

  • setUp() 을 테스트하고 구현했다.
  • 예제 테스트 케이스를 단순화 하기 위해 setUp() 을 사용했다.
  • 예제 테스트 케이스에 대한 테스트 케이스를 단순화 하기 위해 setUp 을 사용했다.

20장. 뒷정리하기

나중에 tearDown 호출하기  

가끔은 setUp() 에서 외부 자원을 할당하는 경우가 있다.

테스트가 계속 서로 독립적이길 바란다면 외부 자원을 할당받은 테스트 들은 작업을 마치기 전에 tearDown() 메소드 같은 곳에서 자원을 다시 반환할 필요가 있다.

단순하게 생각하면 할당 해제를 위한 테스트 방법은 역시 또 하나의 플래그를 도입하는 것이다.

하지만 이제는 이 플래그들이 귀찮게 만들기도 하고 플래그를 사용하는 방식은 메소드의 중요한 면을 하나 놓치고 있다.

바로 setUp() 은 테스트 메소드가 실행되기 전에 호출되야 하고 tearDown() 은 테스트 메소드가 호출된 후에 실행되어야 한다.

여기선 호출된 메소드의 로그를 간단히 남기는 방식으로 테스트 전략을 바꿀 생각이다. 항상 로그의 끝부분에만 기록을 추가한다면 메소드의 호출 순서를 알 수 있게 될 것이다.

# WasRun class

def setUp(self):
  self.wasRun = None
  self.wasSetUp = 1
  self.log = "setUp "

이제 testSetUp() 이 플래그 대신에 로그를 검사하도록 변경할 수 있다.

# TestCaseTest class

def testSetUp(self):
  self.test.run()
  assert("setUp " == self.test.log)

다음으로 이제 wasSetUp 플래그를 지울 수 있다. 그리고 테스트 메소드의 실행을 기록하자.

# WasRun class

def testMethod(self):
  self.wasRun = 1
  self.log = self.log + "testMethod "

이 작업은 TestCaseTesttestSetUp() 을 실패하게 만드니까 다시 수정하자.

# TestCaseTest class

def testSetUp(self):
  self.test.run()
  assert("setUp testMethod " == self.test.log)

이제 이 테스트는 두 개의 테스트가 할 일을 모두 수행한다. 따라서 testRunning 을 지우고 testSetUp 의 이름을 바꿔주자.

# TestCaseTest class

def setUp(self):
  self.test = WasRun("testMethod")
  
def testTemplateMethod(self):
  self.test.run()
  assert("setUp testMethod " == self.test.log)

TestCaseTest 를 보면 WasRun 인스턴스를 한 곳에다가만 사용하니까 다시 분리했던 부분을 돌려놓자.

# TestCaseTest class

def testTemplateMethod(self):
  test = WasRun("testMethod")
  test.run()
  assert("setUp testMethod " == self.test.log)

앞에서 사용했던 것들을 리팩토링 했다가 다시 되돌리는 작업은 자주 있는 일이다. 어떤 사람들은 했던 일을 되돌리는 것을 싫어해서 중복이 서너 번 발생할 때까지 기다리기도 한다.

켄트 백은 사고 주기를 설계에 써버리는 것을 선호하기 때문에 직후에 바로 취소하건 말건 반사적으로 리팩토링한다.

이제 tearDown() 을 테스트 할 준비가 됐다.

# TestCaseTest class

def testTemplateMethod(self):
	test = WasRun("testMethod")
  test.run()
  assert("setUp testMethod tearDown" == test.log)

이렇게 결과가 나와야 하고 실패할 것이니 성공하게 만들자.

# TestCase class

def run(self):
  self.setUp()
  method = getattr(self, self.name)
  method()
  self.tearDown()

def tearDown(self):
  pass
  
# WasRun class

def setUp(self):
  self.log = "setUp "
  
def testMethod(self):
  self.log = self.log + "testMethod "
  
def tearDown(self):
  self.log = self.log + "tearDown "

다음 장에서는 assertion에 관련한 문제가 있음을 파이썬의 에러 핸들링 및 리포팅 시스템이 보고하게 하는 대신 명확한 테스트 실행 결과를 보고할 수 있도록 하는 기능을 다룰 것이다.

그 전에 지금까지 한 것을 검토해보자.

  • 플래그에서 로그로 테스트 전략을 구조 조정했다.
  • 새로운 로그 기능을 이용해서 tearDown() 을 테스트하고 구현했다.
  • 문제를 발견했는데 뒤로 돌아가는 대신 과감히 수정했다.

21장. 셈하기

테스트 메소드가 실패하더라도 tearDown 호출하기  
테스트 여러개 실행하기
수집한 결과를 출력하기

테스트 메소드에서 예외가 발생하건 말건 tearDown() 이 호출되도록 보장해 주는 기능을 구현하려고 했다. 하지만 테스트가 작동하도록 하려면 예외를 잡아야 한다.

일반적으로 테스트를 구현하는 순서는 중요하다. 켄트 백은 다음에 구현할 테스트를 선택할 때, 뭔가 가르침을 줄 수 있고 내가 만들 수 있다는 확신이 드는 것을 선택한다고 한다.

만약 테스트를 하나 성공시켰는데 그 다음 테스트를 만들며 문제가 생기면 두 단계 뒤로 물러서는 것을 고려한다.

우리는 TestCase.run() 이 테스트 하나의 실행 결과를 기록하는 TestResult 객체를 반환하게 만들 것이다.

# TestCaseTest

def testResult(self):
  test = wasRun("testMethod")
  result = test.run()
  assert("1 run, 0 failed" == result.summary())

가짜 구현으로 시작하자.

# TestResult class

class TestResult:
  def summary(self):
    return "1 run, 0 failed"

그리고 TestCase.run()TestResult 를 결과로 반환하도록 하자.

# TestCase class

def run(self):
  self.setUp();
  method = getattr(self, self.name)
  method()
  self.tearDown()
  return TestResult()

이제 테스트가 실행된다. 이제 summary() 의 구현을 조금씩 실체화를 해보자. 우선 실행된 테스트의 수를 상수로 만들 수 있다.

# TestResult class

def __init__(self):
  self.runCount = 1
  
def summary(self):
  return "%d run, 0 failed" % self.runCount

하지만 runCount 가 상수일 수는 없다. 이 수치는 실제로 실행된 테스트 수를 세서 계산되어야 한다. runCount 를 0으로 초기화 하고 테스트가 실행될 때마다 1씩 증가하게 만들자.

# TestResult class

def __init__(self):
  self.runCount = 0
  
def summary(self):
  return "%d run, 0 failed" % self.runCount

def testStarted(self):
  self.runCount = self.runCount + 1

이제 이 메소드를 실제로 호출하게 만들어야 한다.

# TestCase class

def run(self):
  result = TestResult()
  result.testStarted()
  self.setUp();
  method = getattr(self, self.name)
  method()
  self.tearDown()
  return result

실패하는 테스틔의 수를 나타내는 문자열 상수 0을 runCount 처럼 변수로 만들어줘야 하지만 아직 실패하는 테스트가 존재하지 않는다. 그러니 또 다른 테스트를 하나 작성해야 한다.

# TestCaseTest class

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

testBrokendMethod() 는 다음과 같다.

# WasRun class

def testBrokenMethod(self):
  raise Exception 

우리가 관심 가져야 할 사항은 WasRun.testBrokenMethod 에서 던진 예외를 처리하지 않았다는 점이다. 예외를 잡고 테스트가 실패했음을 테스트 결과에 적어 넣고 싶겠지만 잠시 할일 목록에 남겨두자.

지금까지 우리가 한 것을 검토해보자.

  • 가짜 구현을 한 뒤에 단계적으로 상수를 변수로 바꾸어 실제 구현으로 만들었다.
  • 또 다른 테스트를 작성했다.
  • 테스트가 실패했을 때 좀더 작은 스케일을 또 다른 테스트를 만들어서 실패한 테스트가 성공하게 만드는 것을 보조할 수 있었다.

22장. 실패 처리하기

우리는 실패한 테스트를 발견하면 좀더 세밀한 단위의 테스트를 작성해서 올바른 결과를 출력하는 걸 확인하겠다.

# TestCaseTest class

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

testStarted()testFailed() 는 각각 테스트가 시작할 때와 테스트가 실패할 때 보낸 메시지다. 만약 이 메시지들이 위와 같은 순서로 호출되었을 때 summary 문자열이 제대로 출력된다면 우리의 프로그래밍 문제는 어떻게 이 메시지들을 보낼 것인가로 좁혀진다.

일단 메시지를 보내기만 하면 전체가 제대로 동작할 거라는 걸 예상할 수 있다.

구현은 실패 횟수를 세는 것이다.

# TestResult class

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

횟수가 맞다면 우리는 제대로 출력할 수 있다.

# TestResult class

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

이제 testFailed() 만 제대로 호출하면 예상되는 답을 얻을 수 있다. 이걸 호출할 타이밍은 테스트 메소드에서 예외를 잡았을 때다.

# TestCase class

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장. 얼마나 달콤한지

테스트 여러 개 실행하기

여기서 우리는 테스트들을 모아서 한 번에 실행할 수 있는 기능을 원한다. 이를 위해 TestSuite 를 구현해야 한다. 구현할 때 컴포지트 패턴의 순수한 예제를 맛보자.

우린 지금 테스트 하나와 테스트 집단을 동일하게 다루고 싶은 것이다.

TestSuite 를 만들고 거기에 테스트를 몇 개 넣은 다음 이것을 모두 실행하고 그 결과를 얻어내고 싶다.

# TestCaseTest class

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

class TestSuite:
  def __init__(self):
    self.tests = []
    
  def add(self, test)
  	self.tests.append(test)

run() 메소드는 약간 어렵다. 우린 하나의 TestResult 가 모든 테스트에 대해 쓰이길 바라기 때문에 다음과 같이 작성해야 한다.

# TestSuite class

def run(self):
  result = TestResult()
  for test in self.tests:
    test.run(result)
  return result 

이를 해결하는 대안으로 호출하는 곳에서 TestResults 를 할당하는 전략을 이용하겠다. 이 패턴은 매개 변수 수집(collecting parameter)이라 부른다.

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

# TestCaseTest class

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 class

def run(self, result):
  for test in self.tests:
    test.run(result)
# TestCase class

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

이번 장에서 한 일을 검토해보자.

  • TestSuite 를 위한 테스트를 작성했다.

24장. xUnit 회고

자신만의 테스팅 프레임워크를 구현해야 할 때가 온다면 2부에서 보여 준 일련의 과정이 길잡이가 되어 줄 것이다. 세세한 구현 사항 보다는 우리가 사용한 테스트 케이스가 더 중요하다.

이 책에서 보여준 테스트 케이스를 지원할 수 있다면 독립적이고 여러 테스트를 조합할 수 있는 테스트를 작성할 수 있을 것이고, 테스트 우선으로 개발할 준비가 될 것이다.

이 책을 쓸 때 기점에 xUnit은 30개 이상의 프로그래밍 언어에 포팅되어 있다. 이미 우리가 사용하는 언어에 포팅되어 있을 가능성이 높다. 하지만 이미 구현되어 있더라도 직접 xUnit을 구현해볼만한 두 가지 이유가 있다.

  • 숙달: xUnit의 정신은 간결함에 있다. 이에 대해 마틴 파울러는 소프트웨어 공학 역사에서 이토록 많은 사람이 이렇게 짧은 코드로 이토록 큰 은혜를 입은 적이 없었다. 고 말했다. 그러나 몇몇 구현은 조금 복잡해 보인다. 직접 만들어 사용하면 숙달된 도구를 쓰는 느낌을 받을 것이다.
  • 탐험: 새로운 프로그래밍 언어를 처음 접하면 그 언어로 xUnit을 만들어본다. 테스트가 여덟 개에서 열 개 정도 통과할 때쯤이면, 그 언어로 프로그래밍하면서 접하게 될 많은 기능을 한 번씩 경험해보게 된다.

xUnit을 사용하다 보면 assertion 의 실패와 나머지 종류의 에러 사이에 큰 차이점이 있음을 알게 될 것이다. 일반저긍로 assertion failure 가 디버깅 시간을 더 많이 잡아 먹곤 한다. 이런 이유로 xUnit 구현에서는 실패와 에러를 구별한다.

JUnit은 간단한 Test 인터페이스를 선언하는데 TestCaseTestSuite 모두 이를 상속ㅂ다는다. 만약 JUnit 도구가 테스트를 실행하게 만들고 싶다면 Test 인터페이스를 구현하면 된다.

profile
좋은 습관을 가지고 싶은 평범한 개발자입니다.

0개의 댓글