본문 바로가기
멘토링/Codesoom

Test Code를 처음 맛보다

by Alex_beom 2024. 3. 8.
이 글은 필자의 이전 블로그인 https://velog.io/@beomdrive/CodeSoom-3에서 핵심 내용을 중심으로 요약한 후에, 내용을 추가 보완하였습니다.

 

Junit 5 Aseertions ↔ AssertJ

드디어 테스트 코드를 시작했다. 1년 전부터 테스트 코드에 대한 존재를 알게 되면서, 계속 들어왔지만 따로 시간 내서 배워볼 기회가 없어서 너무 배워보고 싶었던 기술이었다. 월요일부터 설레는 마음으로 아침 일찍 노트북을 켜서 강의를 들었다.

강의에서 초반에는 Assertions의 assertEquals()를 사용하면서 첫 번째 인자에는 기대하는 값(expect), 두 번째 인자에는 실제 테스트를 진행할 값 (actual)을 주어야 된다고 설명해 주시더니, 곧바로 AssertJ라는 라이브러리를 사용하였다. 확실히 AssertJ가 좀 더 직관적으로, 우리가 말하는 것처럼 코드를 짤 수 있어서 좋은 것 같았다.

하지만 이것저것 찾아보니 결국엔 다 결은 비슷하고, 사용법만 약간씩 다른 것 같았다. 역시 기술들은 실제 기능을 구현하기 위해 필요한 하나의 도구일 뿐, 큰 맥락은 비슷비슷한 것 같다. (라고 생각하며 쫄지 말자…! 😂)

또한 공식 문서와 친해지기를 이번 주에도 지속될 수 있게 AssertJ 공식 문서부터 먼저 보았더니, 그때그때 찾아보면서 사용하기 너무 좋게 잘 정리되어 있었다. 이제 필요한 문법은 공식 문서를 통해서 확인해 봐야겠다.


Fixture

그 후에는 Fixture라는 개념을 설명해 주셨다. Fixture는 각 테스트들의 준비 과정을 축소시키기 위해, 신뢰할 수 있는 데이터들을 미리 만들어 놓는 데이터의 모음이라고 한다. 따라서 이 fixtures들을 먼저 이해해야 각 테스트를 이해하기 쉽다고 했는데, 여기서 나는 의문이 들었다.

“무작정 Service의 create() 메서드를 통해 작업(Task) 데이터 하나를 만들었는데, 과연 이것은 신뢰할 수 있는 데이터가 될 수 있을까?” 라는 의문이 들었다.
이 fixture 데이터들을 각각의 테스트들에서 사용을 하는데, fixture에 있는 기능 자체가 검증되지 않았다면 테스트 자체가 신뢰성이 떨어질 수 있을 것 같다는 생각이 들었기 때문이다.

이에 @BeforeEach를 통해 fixture를 만드는 메서드(setUp())에다가 검증 로직을 넣고(assertThat), 바로 리뷰어님인 종립 멘토님에게 여쭤봤다.

(아래 질문은 https://github.com/CodeSoom/spring-week3-assignment-1/pull/89#discussion_r1004412349 에서 볼 수 있습니다 😄)

 

 

결론적으로는 회사마다, 팀마다, 상황마다 다 다른 것 같다. 역시 프로그래밍엔 정답은 없는 것 같다…! (그래서 더 어려운 건가… 😂)
나도 앞으로 계속 개발하면서 정답은 없다는 것을 염두하고 균형적인 시각을 가지려 노력해야겠다.

ps. 앞으로 엄격하게 테스트를 구조적으로 분리하며 작업한다는 것은 매우 재밌을 것 같다 😊


BDD 테스트 코드 작성 패턴

테스트 코드를 작성하면서 구글링을 하다가 마침 종립 멘토님의 블로그를 발견했다. Describe - Context - It 패턴을 통해 계층 구조로 테스트 코드를 작성하는 방법을 소개하는 글이었다.

 

바로 적용해 보았는데, 생각보다 익숙하지 않아 어려웠던 것 같다. 특히 Context 절을 작성할 때와 모든 테스트가 한 문장으로 자연스럽게 읽힐 수 있게 테스트 이름(@DisplayName)을 작성하는 게 너무 어려웠던 것 같다 😂

헤매고 있는 와중에 리뷰어님이 조언을 해주셨다.
Describe 절에서는 테스트 대상이 무엇인지를 명시하고, Context 절에서는 테스트 대상에 무엇을 주거나 어떤 환경을 제공하는지를 명시하고, It에는 테스트 대상의 행동을 명시해야 한다고 하셨다.


따라서 내가 정리했던 방식은 파라미터가 없는 경우엔 ~ 때, 파라미터가 있는 경우엔 ~ 주어지면으로 Context절을 시작하도록 해보았다. 그랬더니 나중에 추가되는 하위 테스트 케이스들에도 유연하게 대응할 수 있게 됐던 것 같다.

BDD 스타일로 계속 테스트 코드를 작성하면서 느낀 것이 있다.
계층 구조로 이루어지기 때문에 테스트들을 읽을 때 스코프 범위로 읽을 수 있어 가독성도 좋고 빠트린 부분도 찾기 편하다는 장점이 있지만, 한편으로는 테스트 케이스를 몇 개만 추가해도 코드의 줄이 방대하게 늘어난다는 단점도 있다. (보일러 플레이트 코드들이 너무 많다…)

따라서 필자는 IntelliJ의 Live Templates 기능을 사용하여 보일러 플레이트 코드를 작성하는 것을 최소화하였다.

 

 

여차저차 낑낑대면서 하루 종일 재밌게 코드를 짰더니, 리뷰어님이 매우 따뜻한 리뷰를 남겨주셔서 너무 뿌듯했다.

더욱더 열심히 할 수 있는 원동력이 생겼다 😁


능동적인 의사 결정

내가 이제껏 실무에서 해왔던 것들을 되돌아보면 일을 할 때는 프로젝트를 능동적으로 이끌려고 노력했었던 것 같은데, 정작 코드적인 부분은 매우 수동적이었던 것 같았다.

다시 생각해도 약간 웃긴 것 같다. PM도 아니고 개발자인데 코드적인 부분에서 수동적이라니…

1주차 회고에서 다짐했듯이, 앞으로는 어떤 개발을 하든 항상 돌아가기만 하면 되는 것이 아닌 내부 동작을 파악하며 의식적인 코딩을 하고, 항상 왜라는 의문을 품도록 노력할 것이다.

이번 주에는 감사하게도 리뷰어님이 이런 경험을 연습할 수 있게 도와주셨다.

 

아래 테스트 케이스는 할 일(Task) 수정 시 ID(pk) 값은 절대 변경되지 않는 검증을 하는 코드다.

생각해 보니 상식적으로 일어나지 않을 상황이다. 계속 의식적으로 코딩한다는 것이 아직 안 좋은 습관이 남아있는 것 같다.

종립님 덕분에 의사 결정하는 과정을 트레이닝하고, 커뮤니케이션도 연습할 수 있어서 매우 값진 경험이었던 것 같다.

이 경험을 시작으로 앞으로 계속해서 능동적인 개발을 하고, 생각을 정리하여 의견을 활발하게 내는 연습을 해야겠다고 생각했다.

(아래 질문은 https://github.com/CodeSoom/spring-week3-assignment-1/pull/89#discussion_r1006430973에서 볼 수 있습니다 😄)


Mock은 귀찮은 친구

이번 주 과제에서는 Mock을 통한 테스트와, Mock을 사용하지 않은 테스트를 작성하는 것이 있었다.

  • TaskController 실 객체 테스트
  • TaskController Mock 테스트

두 개의 코드를 작성하면서 문득 “일반 Controller 유닛테스트는 Service 유닛테스트와 다른 점이 뭐지…?”라는 것을 느꼈다. 실제로 Controller는 관련 Service를 호출하기만 할 뿐 별다른 코드가 없었기 때문이다. 차이가 있다면 Mock에서는 given()을 통해 동작을 재정의 해주는 것뿐이기 때문이다.

 

이런 의문이 들었던 이유는 아마 내면에서 실객체 그냥 호출하면 되는데, 왜 굳이 given()으로 메서드 동작을 재정의하는지 이해가 안 됐던 것 같다.

실제로 종립 멘토님은 Mock 테스트를 개인적으로 싫어하여 정말 필요한 상황이 아닌 이상은 지양하신다고 하셨다. 그 이유는 mock을 통해 given()을 사용하여 작동을 재정의하게 되면, 불이 나도 울리지 않는 화재경보기처럼 실제로 발생할 수 있는 버그를 지나치는 상황이 일어날 수 있다고 하셨다.

https://github.com/CodeSoom/spring-week3-assignment-1/pull/89/files#r1008692003

 

[1년 반 지난 시점에서의 견해]

사실 해당 설명 부분은 저 때 당시에는 잘 이해하지 못했던 것 같다. 따라서 지금 생각하는 개인적인 견해를 얘기하면서 이번 글은 마무리하려고 한다.

사실 필자도 Mock을 선호하지 않는다. 그 이유는 아래와 같다.

  1. Mock은 실제 객체의 동작을 재정의 한다. 이 말은 곧 실제 객체의 동작이 변경될 때마다 Mocking 해준 부분도 같이 변경해줘야 한다.
  2. 실제 객체의 최종 Input/Output은 동일하지만, 내부적으로 리팩터링이 들어간다면 해당 Mock 테스트는 깨질 확률이 높다.
  3. 실제 객체의 동작을 잘 이해하지 못한 상태로 Mocking을 하면, 실제 객체에서 버그가 나는데도 테스트 코드는 정상이라고 우리를 속일 수 있다. (False positive)

 

하지만 Mock을 사용해야 되는 상황은 분명 있다. 그 상황은 대략 아래와 같을 것 같다.

  1. 우리가 직접 제어하지 못하는 외부 모듈에는 Mock을 사용한다.
    • 결제 모듈, Message Queue, 일일 호출 한정 API, 타사 API 등
  2. 실제 기능 구현을 하기 전에, 우선적으로 테스트 기반의 API 명세서를 작성해야 할 경우 Mock을 사용한다.
  3. API 명세서뿐만 아니라, 아직 요구 사항이 확정되지 않아 미구현 상태인 메서드와 의존 관계에 있는 기능을 테스트할 때 Mock을 사용한다.

 

따라서 테스트 코드는 상황에 따라서 본인이 그때그때 적합한 방법으로 유연하게 작성할 수 있는 역량을 기르는 것이 제일 중요한 것 같다.

종립님 덕분에 현재 나는 뿌리가 잘 잡혀 어느 정도의 시야가 확보된 것 같다 😁 (아직 많이 부족하지만!)

 

Shout out to 종립 멘토님 🙌

'멘토링 > Codesoom' 카테고리의 다른 글

테스트 방식에 대한 고찰  (0) 2024.03.10
가장 많은 것을 배운 주차  (0) 2024.03.09
TDD의 세계/헥사고날이 뭐예요?  (0) 2024.03.09
Spring F/W 의 소중함을 느끼다  (0) 2024.03.07
Spring F/W 없는 API 개발  (4) 2024.03.07