최근 4개월간 우아한 테크 코스 4기 달록 팀 프로젝트를 진행하였다.
프로젝트 기간이 종료될 쯔음 팀원들과 프로젝트 과정에서 나온 트러블 슈팅에 대해 이야기를 나눠보았다.😮
트러블 슈팅을 이야기하던 중, 우연하게 현재 프로젝트 테스트 코드의 문제점에 대해 이야기를 나누었다.
달록팀은 약 350개의 테스트 코드를 가지고 있다. 트러블 슈팅을 통해 깨달은 350개의 테스트 코드의 문제점은 아래와 같다.
1.
TestFixture
의 개수가 무분별하게 많다.
2.TestMethod
의given
절이 너무 방대하고 복잡하다.
TestCode
의 무분별하게 많아진 TestFixture
는 팀원들의 테스트 관리점만 늘렷고, 복잡한 given
절은 읽기 싫은 테스트 코드를 만들었다.
그래서 "어떻게 하면 관리점이 적고 읽기 좋은 테스트 코드를 만들 수 있을까?" 에 대해 고민한 생각들을 공유해보고자 한다.
우선, 위에서 언급한 두가지 문제점을 달록 프로젝트의 실제 코드를 통해 살펴보자.
첫번째 문제점인 1. TestFixture의 개수가 무분별하게 많다.
부터 살펴보자.
우리 팀은 TestCode
작성 시에 재활용될 만한 DataSet
들을 Fixture
로 관리했다.
특이한 점은 아래와 같이 객체를 생성하는 Method
자체도 Fixture
로 만들었다는 점이다.
사진을 보면 공통_일정()
이라는 네이밍으로 Category
객체를 생성하는 메서드 자체를 Fixture
로 가져갔음을 알 수 있다.
그렇다면 공통_일정()
메서드를 사용하는 TestCode
를 보자.
TestCode
의 208번 줄의 categoryRepository.save()
메서드의 파라미터인 공통_일정()
부분이 Fixture
로 만든 Method
를 활용 한 부분이다.
처음에는 사진과 같이 나름 깔끔한 TestCode
를 만들 수 있었고 팀원들의 만족도도 높았다.
하지만...😥
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
메서드도 함께 늘어나는 문제가 발생했다.
우리 팀 TestCode
의 Fixture
가 가져오는 단점을 살펴보았다.
이어서 두번째 문제였던, 2. TestMethod`의 `given`절이 너무 방대하고 복잡하다.
를 살펴보자.
아래 사진은 달록 Service 객체인 CategoryService
의 TestCode
일부이다.
207번 줄부터 코드를 살펴보자.
한 눈에 보아도 굉장히 복잡한 given
절이 우리를 맞이한다.😤
복잡한 비지니스 로직을 가진 기능을 개발 할 때마다, 위와 같은 복잡한 given
절을 가진 TestMethod
가 반복된다면 어떨까?
아마 Task를 맡은 사람도 TestCode
를 작성한다고 에너지를 다 쓰고, 코드를 리뷰하는 우리도 TestCode
를 리뷰하다가 앓아 누울것이다!😵
TestMethod
의 복잡한 given
절의 문제는 조금 더 자세하게 살펴보면 아래와 같다.
TestMethod
의 가독성을 현저히 떨어트림
복잡한 given
절은 리뷰어에게 하여금 TestMethod
의 목적인 when
절에 집중하는 것을 방해한다.
그럼으로써, TestMethod
의 가독성을 떨어트리고 우리는 어느순간 테스트 코드 리뷰를 안하게 된다.
올바른 TestMethod
인지 인지하기가 어려움
1번 문제에 이어지는 내용이다.
우리가 TestCode
를 볼 때, 일반적으로 given
절을 통해 when
절에 필요한 DataSet
을 파악한다.
그런데, 복잡한 given
절은 우리의 코드 이해를 방해하고 TestMethod
가 정상적으로 프로덕션 코드를 검증하는 TestMethod
인지에 대한 이해를 방해한다.
달록 프로젝트가 가지는 TestCode
의 문제점들을 위에서 살펴보았다.
그렇다면 해결해야 할 부분은 아래와 같다.
1. 객체를 생성하는
Method
는Fixture
로 만들어 사용하지 않는다.
2.TestMethod
의given
절을 최소화 한다.
위에서 언급한 두가지 문제점을 해결하는 방법을 고민해보자!😤
우선, 한가지씩 해결해보기위해 1번 문항과 관련하여 하나의 규칙을 정해보았다.
💡1번 문항에 대한 규칙
객체를 생성하는Method
는 반드시 같은TestClass
내부에Static 하지 않게
선언하고. 한가지로 재활용해서 사용한다.
왜냐하면, 각TestClass
마다 객체의 필드 중 필요한 필드는 다를 수 있기 때문이다.
위 규칙을 가지고 TestCode
의 given
절을 최소화 할 방법을 구체화 하기위한 몇가지 시도를 해보았다.
(시간이 부족하여 실제 프로젝트에는 적용하지 못하고 미션 또는 개인적으로 방법으로 실험 해보았다😔)
이제 본론이다!
하나씩 살펴보자!🔥
첫번째는 Builder 패턴을 활용하는 방법이다.
만약, Builder 패턴을 통해서 given
절의 객체를 생성한다면 메서드체이닝을 통해 생성자를 통한 생성보다는 조금이나마 가독성을 가져 갈 수 있다.
(만약 객체의 필드가 10 ~ 20개가 되면 상당히 유용할 수 도 있을것 같다😤)
그러나 아직도 63번줄의 menuGroupRepository.save()
와 같은 로직은 우리가 when
절에만 온전히 집중하는 것을 방해하고 menuGroupRepository.save()
에 대한 이해를 필요로 한다.
@BeforeEach 구문에 테스트 메서드의 given
절과 관련한 로직을 가능한 최대한 넣는다면 코드를 볼 때, TestMethod
의 given
절을 간소화하고 우리가 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
를 공부해보지 않았지만,메서드 체이닝
방식과 결합한다면 가독성과 성능 두가지 측면을 개선 할 수 있지는 않을까 생각이 든다!
이상으로 테스트 코드 가독성 개선을 위한 생각을 적어보았다!
테스트 코드의 가독성은 굉장히 주관적인 부분이기 때문에 위에 적은 생각들이 맞다고 생각하지 않는다.
하지만, 포스팅을 하면서 읽기 좋은 테스트 코드는 소설 같지 않을까?
라는 생각이 들었다.
소설을 읽듯이 재밌게 읽히는 테스트 코드는 우리가 코드를 작성하거나 리뷰할 때도 즐거움을 줄것이다!
테스트 코드의 가독성을 개선하기 위한 과정에 조금이나마 도움이되는 글이 되었으면 좋겠다!😊
테스트 코드 가독성 검색했는데 리버 블로그네~ ㅋㅋ