어떻게 테스트 코드 가독성을 개선할 수 있을까??

@dallog · November 02, 2022 · 16 min read

이 글은 우테코 달록팀 크루 '리버'가 작성했습니다.

글을 쓴 계기

최근 4개월간 우아한 테크 코스 4기 달록 팀 프로젝트를 진행하였다. 프로젝트 기간이 종료될 쯔음 팀원들과 프로젝트 과정에서 나온 트러블 슈팅에 대해 이야기를 나눠보았다.😮

트러블 슈팅을 이야기하던 중, 우연하게 현재 프로젝트 테스트 코드의 문제점에 대해 이야기를 나누었다.

달록팀은 약 350개의 테스트 코드를 가지고 있다. 트러블 슈팅을 통해 깨달은 350개의 테스트 코드의 문제점은 아래와 같다.

1. TestFixture의 개수가 무분별하게 많다. 2. TestMethodgiven절이 너무 방대하고 복잡하다.

TestCode무분별하게 많아진 TestFixture는 팀원들의 테스트 관리점만 늘렷고, 복잡한 given은 읽기 싫은 테스트 코드를 만들었다.


그래서 "어떻게 하면 관리점이 적고 읽기 좋은 테스트 코드를 만들 수 있을까?" 에 대해 고민한 생각들을 공유해보고자 한다.

기존의 달록 프로젝트 테스트 코드

우선, 위에서 언급한 두가지 문제점을 달록 프로젝트의 실제 코드를 통해 살펴보자.

첫번째 문제점인 1. TestFixture의 개수가 무분별하게 많다. 부터 살펴보자.

우리 팀은 TestCode 작성 시에 재활용될 만한 DataSet들을 Fixture로 관리했다. 특이한 점은 아래와 같이 객체를 생성하는 Method 자체도 Fixture로 만들었다는 점이다.

사진을 보면 공통_일정()이라는 네이밍으로 Category 객체를 생성하는 메서드 자체를 Fixture로 가져갔음을 알 수 있다.

그렇다면 공통_일정() 메서드를 사용하는 TestCode를 보자.

TestCode의 208번 줄의 categoryRepository.save() 메서드의 파라미터인 공통_일정() 부분이 Fixture로 만든 Method를 활용 한 부분이다.

처음에는 사진과 같이 나름 깔끔한 TestCode를 만들 수 있었고 팀원들의 만족도도 높았다.

하지만...😥

달록의 테스트 Fixture가 만난 문제들

1달, 2달 프로젝트가 진행 될 수록, 아래와 같이 무분별하게 Fixture 메서드가 늘어갔다.

한 눈에 봐도 Fixture가 엄청 많다..❗❗ (심지어 한 가지 Domain에 대한 TestFixture 이다.)

재활용 할 수 있는 Fixture들이 많을 것 같은데 모두 어디선가 사용 중이다. 그래서 불필요한 Fixture를 제거하기 위한 리팩터링 비용이 꽤 많이 발생할 것이다.😔

만약 프로젝트 규모가 점점 거대해진다면, 산불 처럼 걷잡을수 없이 Fixture가 늘어날 수 도 있을 것이다. 그때에는 리팩터링 비용은 눈더미처럼 불어날 것이다😤


이렇게 된 원인은 아래와 같다.

** 1. 팀원들 모두 TestCode를 만들 때마다 Fixture 메서드를 생성함 **TestCode 작성은 지루한 작업이다. 그래서 빠르게 TestCode 작성을 끝내기 위해서 기존의 Fixture를 활용하기 보다 필요할 때마다Fixture를 생성하였다. 그러다보니 점점 무분별하게 Fixture의 양이 늘어났다. 심지어 메서드 자체를 Fixture로 만들다 보니 Fixture가 더 많이 불어났다..!


**2. 메서드를 Fixture로 만드는 방법이 효율적이지 않음 **우리 팀은 TestCode의 가독성을 고려하여 객체를 생성하는 메서드 자체가 특정한 한글 이름을 가지도록했다. 사진의 공통_일정(), BE_일정()과 같이 특정한 이름을 가진 Fixture가 그러한 예이다. 그러다 보니, OCP원칙에 어긋나면서 Test를 위한 새로운 객체가 필요할 때 마다 Fixture 메서드도 함께 늘어나는 문제가 발생했다.


우리 팀 TestCodeFixture가 가져오는 단점을 살펴보았다. 이어서 두번째 문제였던, 2. TestMethod`의 `given`절이 너무 방대하고 복잡하다.를 살펴보자.


달록의 테스트 given절이 만난 문제들

아래 사진은 달록 Service 객체CategoryServiceTestCode 일부이다.

207번 줄부터 코드를 살펴보자. 한 눈에 보아도 굉장히 복잡한 given절이 우리를 맞이한다.😤

복잡한 비지니스 로직을 가진 기능을 개발 할 때마다, 위와 같은 복잡한 given절을 가진 TestMethod가 반복된다면 어떨까?

아마 Task를 맡은 사람도 TestCode를 작성한다고 에너지를 다 쓰고, 코드를 리뷰하는 우리도 TestCode를 리뷰하다가 앓아 누울것이다!😵


TestMethod의 복잡한 given절의 문제는 조금 더 자세하게 살펴보면 아래와 같다.

  1. TestMethod의 가독성을 현저히 떨어트림 복잡한 given절은 리뷰어에게 하여금 TestMethod의 목적인 when절에 집중하는 것을 방해한다. 그럼으로써, TestMethod의 가독성을 떨어트리고 우리는 어느순간 테스트 코드 리뷰를 안하게 된다.
  2. 올바른 TestMethod인지 인지하기가 어려움 1번 문제에 이어지는 내용이다. 우리가 TestCode를 볼 때, 일반적으로 given절을 통해 when절에 필요한 DataSet을 파악한다. 그런데, 복잡한 given절은 우리의 코드 이해를 방해하고 TestMethod정상적으로 프로덕션 코드를 검증하는 TestMethod인지에 대한 이해를 방해한다.

문제점들을 해결할 방법은?

달록 프로젝트가 가지는 TestCode의 문제점들을 위에서 살펴보았다. 그렇다면 해결해야 할 부분은 아래와 같다.

**1. 객체를 생성하는 MethodFixture로 만들어 사용하지 않는다.

  1. TestMethodgiven절을 최소화 한다.**

TestCode 문제점 해결 방법 구체화하기

위에서 언급한 두가지 문제점을 해결하는 방법을 고민해보자!😤 우선, 한가지씩 해결해보기위해 1번 문항과 관련하여 하나의 규칙을 정해보았다.

💡1번 문항에 대한 규칙 ** 객체를 생성하는 Method는 반드시 같은 TestClass 내부에 Static 하지 않게 선언하고. **한가지로 재활용해서 사용한다. 왜냐하면, 각 TestClass마다 객체의 필드 중 필요한 필드는 다를 수 있기 때문이다.

위 규칙을 가지고 TestCodegiven절을 최소화 할 방법을 구체화 하기위한 몇가지 시도를 해보았다. (시간이 부족하여 실제 프로젝트에는 적용하지 못하고 미션 또는 개인적으로 방법으로 실험 해보았다😔)

이제 본론이다! 하나씩 살펴보자!🔥


구체화한 개선 방법들

Builder 패턴의 활용

첫번째는 Builder 패턴을 활용하는 방법이다. 만약, Builder 패턴을 통해서 given 절의 객체를 생성한다면 메서드체이닝을 통해 생성자를 통한 생성보다는 조금이나마 가독성을 가져 갈 수 있다. (만약 객체의 필드가 10 ~ 20개가 되면 상당히 유용할 수 도 있을것 같다😤)

그러나 아직도 63번줄의 menuGroupRepository.save() 와 같은 로직은 우리가 when절에만 온전히 집중하는 것을 방해하고 menuGroupRepository.save()에 대한 이해를 필요로 한다.


@BeforeEach 구문의 활용

@BeforeEach 구문에 테스트 메서드의 given 절과 관련한 로직을 가능한 최대한 넣는다면 코드를 볼 때, TestMethodgiven절을 간소화하고 우리가 when절에 집중 하는 것을 도울 수 있다.

그럼에도 불구하고 여전히 90번 줄의 orderTableRepository.save() 와 같은 DB 저장 로직은 제거 할 수 없고 우리가 when절에만 온전히 집중하는 것을 방해하고 있다.

위 두가지 방식은 orderTableRepository.save() 와 같은 DB 저장 로직을 제거하지 못한다. 그렇다면 orderTableRepository.save() 와 같은 로직을 제거 할 방법은 뭐가 있을까??


메서드 체이닝 방식의 활용

내가 찾은 방법은 메서드 체이닝 방식의 활용이다. Builder 패턴의 메서드 체이닝을 보면서 힌트를 얻었다.

만약, 메서드 체이닝 방식으로 orderTableRepository.save() 와 같은 로직을 숨긴다면 더 가독성 좋은 given을 만들 수 있지 않을까 하는 생각이였다.

또한, 한글 네이밍의 메서드들을 병렬로 배치함 으로써 마치 소설을 읽듯이 잘 읽히는 given절을 만들 수 있지 않을까하는 생각이 들었다.

한번 코드로 자세히 살펴보자.🦾🦾

우선, 기존의 달록 프로젝트 테스트 코드 일부를 보자.

놀랍게도 하나의 테스트 메서드이다.🙄 만약 이 코드를 본다면 넓디 넓은 given에서 우리는 이미 지쳐 버릴 것이다.

왜 그럴까? 앞서 설명한 방대한 given절이 가져오는 문제 때문이다.

이 테스트 메서드에 메서드체이닝 방식을 적용해보자.

위 사진은 기존 테스트 메서드를 메서드체이닝 방식으로 리팩터링 한 코드이다. _ orderTableRepository.save() 같은 메서드가 제거되면서 given절이 굉장히 간소화 됬다! 그리고 한글문장이 소설처럼 이어지면서 given절을 통해 프로덕션의 비지니스 로직이 이해되기도 한다! 마지막으로 given절 과 when절의 구분이 명확하게 느껴진다!_

그럼 메서드 체이닝을 하는 객체를 살펴보자. 첫번째 규칙에 따라서 TestClass 내부의 InnerClass로 객체 종류에 따라 한가지 메서드만 정의해보았다.

사진과 같이 InnerClass에 체이닝 방식을 위한 메서드들을 만들고 xxxRepository.save()와 같은 로직, 객체를 숨겨놓았다.

최종적으로 다시 차이를 살펴보자! ** 위와 같이 복잡한 given절이 필요한 경우에도 메서드 체이닝을 통해서 **아래와 같이 간소화 시킬 수 있다.

내가 느낀 메서드 체이닝 방식의 장점

직접 프로젝트 코드 일부에 메서드체이닝 방식을 실험 해보면서 느낀 장점을 정리해보자!😤

  • 다른 객체의 메서드 호출(ex. xxxRepository.save())을 숨김으로써 given 절을 간소화 할 수 있다.
  • 소설을 읽듯 한글로 이어지는 given 절의 메서드 체이닝으로 프로덕션 비지니스 로직의 이해에 도움이 된다.
  • 코드를 읽는 입장에서 when절 테스트를 위해 필요한 given절의 DataSet이 한눈에 들어온다.

물론 장점과 더불어 단점도 있다고 생각한다.

내가 느낀 메서드 체이닝 방식의 단점

  • 메서드 체이닝을 위한 객체를 생성하는 비용이 발생하고 관리점이 늘어난다.
  • 도메인 객체에 변화가 생기면 메서드 체이닝 객체도 함께 수정 해주어야한다.

메서드 체이닝 방식을 ServiceTest에만 적용 해보았지만, 인수테스트, 단위 테스트 등에도 적절하게 사용할 수 있지 않을까 생각한다!

**💡 DynamicTest메서드체이닝의 결합? **우연히 크루분의 코드를 보다가 DynamicTest를 만드신 코드를 보았다. 아직 DynamicTest를 공부해보지 않았지만, 메서드 체이닝 방식과 결합한다면 가독성과 성능 두가지 측면을 개선 할 수 있지는 않을까 생각이 든다!

마치면서

이상으로 테스트 코드 가독성 개선을 위한 생각을 적어보았다! 테스트 코드의 가독성은 굉장히 주관적인 부분이기 때문에 위에 적은 생각들이 맞다고 생각하지 않는다.

하지만, 포스팅을 하면서 읽기 좋은 테스트 코드는 소설 같지 않을까? 라는 생각이 들었다.

소설을 읽듯이 재밌게 읽히는 테스트 코드는 우리가 코드를 작성하거나 리뷰할 때도 즐거움을 줄것이다!

테스트 코드의 가독성을 개선하기 위한 과정에 조금이나마 도움이되는 글이 되었으면 좋겠다!😊

@dallog
우아한테크코스 4기 달록팀 기술 블로그입니다.