코드숨 3주차 - Test Code, 그 속으로

beomdrive·2022년 11월 1일
0

멘토링

목록 보기
3/8

“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), 바로 이번주 리뷰어님인 종립 멘토님에게 여쭤봤다.

@BeforeEach
void setUp() { // fixture 생성 메서드
	  ... 생략 ...
    Task task = taskService.createTask(source); //fixtrue 생성
    
    assertThat(task.getId()).isEqualTo(TEST_ID); // fixture 데이터 검증
}

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

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


“BDD 테스트 코드 작성 패턴”

테스트 코드를 작성하면서 구글링을 하다가 마침 종립 멘토님의 블로그를 발견했다.
Describe - Context - It 패턴을 통해 계층 구조로 테스트 코드를 작성하는 방법을 소개하는 글이었다.
바로 적용해보았는데, 생각보다 익숙하지 않아 어려웠던 것 같다.

특히 Context 절을 작성할 때와, 모든 테스트가 한 문장으로 자연스럽게 읽힐 수 있게 테스트 이름(@DisplayName)을 작성하는게 너무 어려웠던 것 같다 😂

헤매고 있는 와중에 리뷰어님이 조언을 해주셨다.

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

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

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

이런 점을 봤을 땐 확실히 개발팀마다 취향이 갈릴 것 같은 느낌이 들었다.

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

더욱 더 열심히 해야겠다는 마음이 들었다 😊


“능동적인 의사 결정”

내가 이제껏 실무에서 해왔던 것들을 되돌아보면 일을 할 때는 프로젝트를 능동적으로 이끌려고 노력했었던 것 같은데, 정작 코드적인 부분은 매우 수동적이였던 것 같았다.
다시 생각해도 약간 웃긴 것 같다. PM도 아니고 개발자인데 코드적인 부분에서 수동적이라니…

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

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

해당 테스트 케이스는 할 일(Task)을 수정하는 Service 로직을 테스트하는 것인데, ID(pk)값을 변경해서 넘겨줘도 실제 수정에는 영향이 없는가를 테스트한 코드이다.
생각해보니 일어나지 않을 상황을 테스트 케이스로 작성했다. 계속 의식적으로 코딩한다는 것이 아직 안 좋은 습관이 남아있는 것 같다.

일단 이 테스트가 왜 일어나지 않을 케이스인지 먼저 분석하였다.
일반적으로 pk값 수정을 허용하지 않는 이유와, 수정 API의 RequestDto 모습 등을 정리했다. 그 후 실제 구현되어 있는 service 로직의 흐름 또한 정리하였다.
마지막으로 정리한 상황들을 가지고 팀원분과 커뮤니케이션 하듯이 설명한 후, 해당 테스트를 @Disabled 처리하고 커밋을 날렸다.

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


“Mock은 귀찮은 친구”

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

  • TaskController 유닛 테스트
  • TaskController MockMVC 테스트

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

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

사실 글만 봤을 때는 잘 와닿지 않았는데, 감사하게 주신 추가 과제를 구현하면서 기존 기능을 수정하다가 경험한 것이 있다.
실제로 동일한 Controller 테스트인데, 구현을 수정했더니 실제 객체를 통한 테스트는 올패스가 떨어지고 Mock을 사용한 쪽에서는 테스트가 깨졌었다.

발생할 수 있는 버그를 지나치는 상황은 아니였지만, 기존 구현을 변경하게 되면 Mock 테스트도 다 변경해주어야되는 것을 느끼면서 약간의 걱정이 되었다.
실무에서 엄청 복잡한 Service에서 어떤 기능을 추가해야될 때, 해당 테스트가 Mock으로 덕지덕지 되어있다면..?
생각만 해도 아찔했다.

이 때 Mock에 대한 경각심이 피부에 와닿아서, Mock을 정말 필요한 상황(미구현 모듈, 외부 모듈 등)에서만 사용을 하고, 웬만하면 실제 객체를 사용하는 것이 안전해보였다. (또는 FakeObject...)

추가로 mock()과는 별개로 spy()라는 기능도 풀이 강의에서 아샬님이 소개해주셨다.

spy()에 대해서 이것저것 알아본 결과, 오히려 mock()을 사용할 바에는 spy()를 사용하는 것이 더 좋아 보였다. mock()은 껍데기 빈을 가져와서 일일히 모든 기능을 given으로 재정의해줘야 하는데, spy()는 필요한 부분에만 given을 사용하고 나머지는 실제 객체로 사용할 수 있기 때문이다.

테스트를 많이 짜보면서 실제로 given을 꼭 사용되어야 될 때만 사용하도록 판단하는 연습도 해야겠다고 생각했다.

profile
꾸준함의 가치를 향해 📈

0개의 댓글