약 4개월 전, 이전에 일하고 있던 회사에서 퇴사를 했다. 당시 회사에서는 기존에 진행했던 프로젝트를 마무리하고 새 프로젝트를 진행할 예정이었는데, 오래전부터 퇴사를 마음먹고 있었던지라 새 프로젝트 중간에 퇴사하는 것보다 깔끔하게 퇴사하는 것이 낫겠다 싶어서 다음이 정해지지 않은 상태에서 과감하게 퇴사를 결정했다. 퇴사를 결정하게 된 사유 중에는 잦은 요구사항 변경 요청이 있었다.
퇴사 전 프로젝트를 진행했던 팀에서는 테스트 코드 작성을 하지 않았었다. 아니, 할 생각조차 못했다고 말하는 것이 맞을 것이다. 팀은 고객사의 잦은 요구사항 변경, 팀에서 준비되지 않은 기술을 강제로 사용해야 하는 등 여유가 없는 상태였으니. 아무튼, 이런저런 핑계로 테스트 코드 작성은 외면받는 상황이었다. 그러한 상황에서 새로운 요구 사항을 위해 코드를 수정하면 가끔씩 이전에 동작하던 기능에서 문제가 발생하곤 했고 프로젝트가 진행될 수록 이는 더더욱 잦아졌다. 무엇보다 가장 큰 문제는 그 때 당시의 나는 테스트 코드의 존재를 몰랐었다.
문제의 나비효과
요구사항이 발생하면 해당 요구사항에 맞게 코드를 수정 및 보완한다. 코드를 수정하지 않고 해결할 다른 방법이 있지 않는 이상 지극히 당연한 명제이다. 다만 동시에 고려해야 할 명제가 있는데 그것은 바로 ‘이전에 동작하던 기능에는 문제가 없어야 한다’ 는 명제이며 정책이 완전히 바뀌지 않는 상황이 아니라면 두 명제는 대부분의 경우 && 이다. 문제는 코드를 수정 및 보완하는 작업은 언제나 이전에 동작하던 기능을 건드리는 위험에 항상 노출되어 있다는 것인데 위에서 언급했듯이 이는 코드가 누적될 수록 심해진다.
테스트 코드의 존재를 알게 되다.
테스트 코드라는 것을 처음 알게 된 것은 JPA를 공부하기 위해 인프런에 있는 김영한 님의 강의를 수강하면서부터였다. 강의에서는 테스트 코드를 Given-When-Then 의 단계로 구분하여 코드를 작성하고 여러 경우의 간단한 테스트 코드를 통해 검증하는 모습을 보여주었는데 그 중 눈에 띄는 부분은 ‘실패 테스트 코드’ 작성이었다.
코드의 실패를 고민해야한다.
프로그램은 여러가지 이유로 예외가 발생한다. 파라미터의 타입이 맞지 않거나 요청에 필요한 권한이 없거나 존재하지 않는 리소스를 요청하는 등 여러 경우의 수가 존재하며 개발자는 이를 최대한 대처해야 할 의무가 있다. 실패할 상황에 대해 고민하고 그에 대한 대비책을 마련해야한다. 이를 외면한다면 프로그램은 버그투성이에 방치되고 해당 프로그램을 서비스하는 회사는 폐업할 것이며 개발자는 해고 통보를 받게 될 것이다.
실패 테스트 코드를 작성한다는 것은 바꿔말하면 실패할 수 있는 상황을 고민한다는 것과 같다. 작성한 코드가 어떤 상황에서 실패할 수 있는지, 실패하는 상황을 대처할 코드는 작성되어 있는지 점검하는 기회를 제공해 주는 것이다. 돌이켜보면 테스트 코드의 존재를 몰랐던 시기의 나는 확실히 이 기회를 놓치고 있었다.
코드는 누적된다. 테스트 코드 역시 마찬가지다.
테스트 코드의 존재를 알게 된 이후로 개인 프로젝트를 진행하면서 테스트 코드를 작성하는 습관을 들이기 시작했다. 아직 ‘테스트 주도 개발’ 같은 책을 읽어보거나 제대로 공부한 것은 아니지만 나름대로 테스트 코드를 작성해나갔다. 테스트에 필요한 값을 설정하고, 실행하고, 실행 결과를 확인하는 코드들을 작성했다.
프로젝트가 진행될 수록 코드는 누적되었고 테스트 코드 역시 코드이기에 마찬가지였다. 오히려 테스트 코드의 양이 더 많아져 처음에는 테스트 코드가 정말 효율이 있나 싶었지만 이런 고민은 얼마 지나지 않아 더 이상 할 필요가 없음을 깨달았다. 누적된 테스크 코드는 수정 및 변경 작업 후 다시 실행시켜 봄으로써 이전의 기능들이 이상없이 동작하는지, 이상이 발생했다면 수정된 코드가 어떤 문제를 발생시키는지 점검해주는 좋은 도구가 되어주었기 때문이다.
@Test
@DisplayName("잘못된 형식의 값으로 저장")
void postPet_wrongFieldTypes() throws Exception{
System.out.println("PostTest.postPet_wrongFieldTypes");
// given
PetAdd petAdd = PetAdd.builder()
.name("dog 123")
.weight(-10.5)
.age(0)
.categories(List.of("야옹이", "길냥이", "혼종"))
.build();
session.setAttribute(LoginAccount.value, new SessionAccount(account));
// when & then
mockMvc.perform(post("/pets")
.contentType(APPLICATION_JSON)
.content(mapper.writeValueAsString(petAdd))
.with(attributes)
.session(session)
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(jsonPath("$.message").value(ExceptionController.BIND_EXCEPTION_MESSAGE))
.andExpect(jsonPath("$.validation.name").value(NamePattern.MESSAGE))
.andExpect(jsonPath("$.validation.weight").value(PositiveNumberPattern.MESSAGE))
.andExpect(jsonPath("$.validation.categories").value(CategoryPattern.MESSAGE))
.andExpect(jsonPath("$.validation.age").value(PositiveNumberPattern.MESSAGE));
}
@Test
@DisplayName("펫 정보 저장")
void postPet() throws Exception {
System.out.println("PostTest.postPet");
// given
PetAdd petAdd = PetAdd.builder()
.name("cat")
.weight(10.5)
.age(3)
.categories(List.of(PetType.CAT.name(), CatKindCategory.NORWEGIAN.name(), Gender.FEMALE.name()))
.build();
session.setAttribute(LoginAccount.value, new SessionAccount(account));
// when
mockMvc.perform(post("/pets")
.contentType(APPLICATION_JSON)
.content(mapper.writeValueAsString(petAdd))
.with(attributes)
.session(session)
)
.andExpect(status().isOk());
// then
assertThat(petRepository.count()).isEqualTo(1);
}
마치며...
최근에 백기선 님의 유튜브 영상 중 ‘더 나은 개발자로 성장하는 팁 - 나는 그런 개발자가 좋더라’ 을 보게 되었는데 그 영상에서 백기선 님은 ‘테스트 코드를 잘짜는 개발자의 코드를 신뢰한다’ 라고 말씀하셨었다. 그 말을 듣고 과거의 나에게 질문해보았다.
‘내가 작성한 코드는 신뢰할 수 있었나?’
아니요. 답은 명백했고 실제로도 그랬다. 과거의 테스트 코드를 작성하지 않아서 겪었던 경험이 실패 테스트 코드로서 명백히 말해주고 있다.