보통 TDD는 다음과 같은 절차를 가진다.
내가 TDD를 싫어하는 이유다.
하지만 나는 TDD를 사랑한다. 무슨 헛소리냐
나는 TDD의 본질이 저 방식이라고 생각 안한다.
이 시리즈의 1, 2, 3편을 봤다면 요구사항과 계약을 지키고 구현하는 게 중요함을 알 것이다.
우리는 계약을 소프트웨어에서 인터페이스로 적용한다.
그런데 그 계약을 실제로 지키는지 어떻게 알지?
인터페이스를 따른다고 내가 원하는 요구사항을 만족한건가?
부족하다.
보통 요구사항과 계약을 구현하고 지키는지 테스트 하는데
결국엔 계약을 지켜야되는게 목표가 아닌가?
그럼 계약을 지키는지 검사하는 테스트를 먼저 만들면 되는거 아닌가?
TDD는 계약의 검증이다. TDD는 구현이 계약을 만족하는지 검사하는 진정한 의미의 테스트이다.
테스트를 통과하지 못한다면 그건 실패한 구현이다. 왜? 계약을 지키지 못했으니
내가 왜 TDD를 싫어하는지 이해가 되나? TDD는 계약의 검증이다. 살아있는 보증서이고 변화하는 문서이다. 근데 일부러 틀리는 코드를 작성한다?
TDD의 'RED' 단계는 실패를 목표로 삼는 것이 아니다. 단지 '지켜야 할 계약(테스트)'을 먼저 정의했기 때문에, 아직 그 계약을 만족시키는 구현체가 없어 '자연스럽게' 실패 상태로 시작하는 것뿐이다. '실패'는 목표가 아니라, 계약 이행 전의 당연한 상태에 불과하다. 이를 '일부러 틀린다'고 표현하는 것은 TDD의 본질을 왜곡하고 주객을 전도시키는 설명 방식이다.
이 시리즈의 1, 2, 3편을 읽었다면 내가 요구사항과 계약을 중요하게 생각함을 이해할 것이다.
테스트를 먼저 작성하든, 구현을 하고 작성하든 계약을 검증한다는 점에서 나는 TDD를 사랑한다. 나는 아래와 같은 방식으로 TDD를 이용하고 있다.
이해하기 쉽게 내가 VO 클래스를 단위 테스트한 파일을 보여주겠다.
class TestActorValueObject(unittest.TestCase):
def test_create_valid_actor_all_fields(self):
# 계약: 모든 필드가 유효한 값으로 ActorVO를 성공적으로 생성할 수 있어야 한다.
vo = ActorVO(name="톰 행크스", role_name="포레스트 검프", external_id="tmdb_actor_123")
self.assertEqual(vo.name, "톰 행크스")
self.assertEqual(vo.role_name, "포레스트 검프")
self.assertEqual(vo.external_id, "tmdb_actor_123")
def test_create_valid_actor_name_only(self):
# 계약: 필수 필드인 이름만으로 ActorVO를 성공적으로 생성할 수 있어야 한다.
vo = ActorVO(name="송강호")
self.assertEqual(vo.name, "송강호")
self.assertIsNone(vo.role_name)
self.assertIsNone(vo.external_id)
def test_create_valid_actor_name_and_role(self):
# 계약: 이름과 배역명으로 ActorVO를 성공적으로 생성할 수 있어야 한다.
vo = ActorVO(name="최민식", role_name="이순신")
self.assertEqual(vo.name, "최민식")
self.assertEqual(vo.role_name, "이순신")
self.assertIsNone(vo.external_id)
def test_create_with_empty_name_raises_value_error(self):
# 계약: 비어있는 이름으로 생성 시도 시 ValueError가 발생해야 한다.
with self.assertRaisesRegex(ValueError, "배우 이름은 비어있을 수 없습니다."):
ActorVO(name="")
def test_create_with_name_too_long_raises_value_error(self):
# 계약: 이름이 최대 길이(100자)를 초과하면 ValueError가 발생해야 한다.
with self.assertRaisesRegex(ValueError, "배우 이름은 유효한 문자열이어야 하며 최대 100자입니다."):
ActorVO(name="가" * 101)
def test_create_with_role_name_too_long_raises_value_error(self):
# 계약: 배역명이 최대 길이(100자)를 초과하면 ValueError가 발생해야 한다.
with self.assertRaisesRegex(ValueError, "배역명은 최대 100자까지 가능합니다."):
ActorVO(name="배우이름", role_name="가" * 101)
def test_create_with_external_id_too_long_raises_value_error(self):
# 계약: 외부 ID가 최대 길이(100자)를 초과하면 ValueError가 발생해야 한다.
with self.assertRaisesRegex(ValueError, "외부 ID는 최대 100자까지 가능합니다."):
ActorVO(name="아이디테스트배우", external_id="a" * 101)
def test_create_with_non_string_types_raises_type_error(self):
# 계약: 이름, 배역명, 외부 ID가 문자열이 아닐 경우 TypeError가 발생해야 한다.
with self.assertRaisesRegex(TypeError, "배역명은 문자열이어야 합니다."):
ActorVO(name="타입테스트배우", role_name=123)
with self.assertRaisesRegex(TypeError, "외부 ID는 문자열이어야 합니다."):
ActorVO(name="타입테스트배우", external_id=True)
def test_optional_fields_can_be_none(self):
# 계약: 선택적 필드(role_name, external_id)는 None으로 설정될 수 있어야 한다.
try:
vo = ActorVO(name="필수이름만배우", role_name=None, external_id=None)
self.assertIsNone(vo.role_name)
self.assertIsNone(vo.external_id)
except Exception as e:
self.fail(f"선택적 필드에 None 할당 시 예외 발생: {e}")
def test_actor_equality(self):
# 계약: 두 ActorVO 객체는 모든 속성 값이 같으면 동등해야 한다.
vo1 = ActorVO(name="동일배우", role_name="같은역할", external_id="ext1")
vo2 = ActorVO(name="동일배우", role_name="같은역할", external_id="ext1")
vo3 = ActorVO(name="다른배우", role_name="같은역할", external_id="ext1")
vo4 = ActorVO(name="동일배우", role_name="다른역할", external_id="ext1")
vo5 = ActorVO(name="동일배우")
vo6 = ActorVO(name="동일배우")
self.assertEqual(vo1, vo2)
self.assertNotEqual(vo1, vo3)
self.assertNotEqual(vo1, vo4)
self.assertEqual(vo5, vo6)
self.assertNotEqual(vo1, vo5)
def test_actor_hash_consistency(self):
# 계약: 동등한 ActorVO 객체는 동일한 해시 값을 가져야 한다.
vo1 = ActorVO(name="해시값배우", role_name="역할1", external_id="hash_ext1")
vo2 = ActorVO(name="해시값배우", role_name="역할1", external_id="hash_ext1")
self.assertEqual(hash(vo1), hash(vo2))
def test_actor_string_representation(self):
# 계약: ActorVO 객체의 문자열 표현은 적절히 표시되어야 한다.
vo_name_only = ActorVO(name="이병헌")
self.assertEqual(str(vo_name_only), "이병헌")
vo_with_role = ActorVO(name="마동석", role_name="마석도")
self.assertEqual(str(vo_with_role), "마동석 (배역: 마석도)")
나는 위와 같이 설계하다나온 계약, 제약사항을 주석을 통해 명시한다.
내가 ActorVo를 어떻게 구현하든, 어떻게 바꾸든 저 계약을 지켜야한다.
저 테스트는 그걸 보증하는 살아있는 문서다.
만약 요구사항 자체가 바뀌어서 계약이 바뀐다? 그러면 테스트를 수정하면 그만이다. 물론 주석으로 바뀐 계약을 명시한채
처음 테스트 코드를 작성할 때 가장 이해가 안 되는게 바로 assert다.
결과를 만들고 예상하는거랑 맞는지 뭐하러 검사하는 거지? 아무 쓸 때 없는 짓 아닌가?
하지만 시리즈 1, 2, 3에서 보듯 요구사항과 계약이 중요하지
assert의 뜻은 단언하다란 뜻이다.
왜 단언하는 게 테스트에서 필요하나?
바로 계약이 맞는지 검증해야 하니까
assert(내가 구현한 결과, 계약이 요구하는 결과값)
이 단언은 곧 (내가 구현한 결과 == 계약 만족값)
assert야 말로 테스트의 목적인 계약의 검증을 나타내는 직관적인 표현이다.
TDD는 객체지향의 조력자고 요구사항과 계약을 지키는지 검사하는 살아있는 문서다.