클린 코드의 테스트 코드에 대한 부분을 읽으면서 이전에 짠 테스트 코드가 "쓸데없이 복잡"해서 어디를 봐야할지 모르겠다는 생각이 들었다. freeboard2 애플리케이션의 테스트 코드를 리펙토링해보려고 한다.
우선 클린 코드의 9장인 단위 테스트의 내용을 정리한다.
지저분한 테스트 코드는 테스트를 안하는 것과 마찬가지, 혹은 그 이하일 수도 있다. 문제는 실제 코드가 진화하면 테스트 코드도 변해야한다는 것인데 테스트 코드가 지저분할수록 변경하기 어려워진다.
테스트 코드는 실제 코드 못지 않게 중요하다. 테스트 코드는 이류 시민이 아니다. 테스트 코드는 사고와 설계와 주의가 필요하다. 실제 코드 못지 않게 깨끗하게 짜야한다.
코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트이다. 이유는 단순하다. 테스트 케이스가 있으면 변경이 두렵지 않으니까! 테스트 케이스가 없다면 모든 변경이 잠정적인 버그다. 즉, 테스트 케이스가 있으면 변경이 쉬워진다.
테스트 코드가 지저분하면 코드를 변경하는 능력이 떨어지며 코드 구조를 개선하는 능력도 떨어진다. 테스트 코드가 지저분할수록 실제코드도 지저분해진다. 결국 테스트 코드를 잃어버리고 실제 코드도 망가진다.
깨끗한 테스트 코드를 만들려면 세 가지가 필요하다. 가독성, 가독성, 가독성.
테스트 코드에서 가독성을 높이려면? 여느 코드와 마찬가지다. 명료성, 단순성, 풍부한 표현력이 필요하다. 테스트 코드는 최소의 표현으로 많은 것을 나타내야 한다.
중복되는 코드와 자질구레한 사항이 너무 많으면 테스트 코드의 표현력이 떨어진다. 테스트와 무관하면 테스트 코드의 의도도 흐려진다. 이런 코드를 읽는 사람들은 온갖 잡다하고 무관한 코드를 이해한 후에야 간신히 테스트 케이스를 이해할 것이다.
❗️NOTE
BUILD-OPERATE-CHECK 패턴
첫 부분은 테스트 자료를 만든다. 두 번째 부분은 테스트 자료를 조작하며, 세 번째 부분은 조작한 결과가 올바른지 확인한다.
테스트 API 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 확실히 다르다. 단순하고, 간결하고, 표현력이 풍부해야 하지만, 실제 코드만큼 표율적일 필요는 없다.
JUnit으로 테스트 코드를 짤 때는 함수마다 assert 문을 단 하나만 사용해야 한다고 주장하는 학파가 있다. 결론이 하나기 때문에 코드를 이해하기 쉽다. 반면에 하나였던 테스트를 분리하면 중복되는 코드가 많아진다.
어쩌면 "테스트 함수마다 한 개념만 테스트하라"는 규칙이 더 낫겠다. 이것저것 잡다한 개념을 연속으로 테스트하는 긴 함수는 피한다.
즉, assert 문이 여럿이라는 사실은 문제가 덜하다. 오히려 한 테스트 함수에서 여러 개념을 테스트한다는 것이 문제다. 가장 좋은 규칙은 개념 당 assert 문 수를 "최소"로 줄여라와 테스트 함수 하나는 개념 하나만 테스트하라이다.
테스트는 빨리 돌아야한다. 테스트가 느리면 자주 돌릴 엄두는 못내고 이는 초반에 문제를 찾아내 고치지 못하게 한다.
각 테스트는 서로 의존하면 안된다. 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안된다. 각 테스트는 독립적으로 그리고 어떤 순서로 실행해도 괜찮아야한다.
테스트는 어떤 환경에서도 반복 가능해야 한다. 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다.
테스트는 성공 아니면 실패다. 테스트는 스스로 성공과 실패를 가늠해야한다.
테스트는 적시 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다. 실제 코드를 구현한 후에 테스트 코드를 작성하면 그 코드는 테스트하기 너무 어렵거나 불가능할지도 모른다.
테스트 코드는 어쩌면 실제 코드보다 더 중요할지도 모른다. 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화하기 때문이다. 그러므로 테스트 코드는 지속석으로 깨끗하게 관리하자. 표현력을 높이고 간결하게 정리하자.
내가 책을 읽으면서 가장 먼저 떠올린 것은 다음의 테스트 코드이다. (BoardMapperTest 클래스)
너무나도 장황한 이 코드는 심지어 다른 테스트 코드에서도 반복되고 있다.
미묘한 차이는 있지만 거의 비슷한 구조이다. 이 코드들을 최대한(!) 고쳐보도록 하겠다.
실제 테스트 코드를 이해하는데 방해가 되는 것들을 추려내도록 하자 🤔
위 코드는 살짝 문제가 있는데, 토탈페이지 계산을 위해 카운팅이 잘 되었는지 판단하는 assert문은 마지막 것 뿐이고, 그 위의 두개는 무관한 테스트이다.
유효한 assert문인 assertThat(totalCount, equalTo(insertSize))
에서 사용되는 변수는 totalCount
와 insertSize
뿐이고 그 외에 수많은 변수들을 테스트 코드를 이해하는데 방해만 될 뿐이다.
두 assert문을 제거하자 findEntities를 가져올 필요가 없어졌다. 초록색 블럭 위의 로직은 모두 테스트를 위해 유일한 데이터(현재 시간을 제목이나 내용으로 포함한다.)를 새로 insert하는 로직이다. 즉, 사용자가 굳이 세세하게 알 필요가 없다는 것이다. 이 부분은 다른 메소드로 빼내도록 하겠다.
검색이 잘 되었는지 확인할 때 사용할 INSERTSIZE
와 유일한 검색어로 사용할(새로 추가한 데이터"만" 식별해 내기위함) TIME
을 파라미터로 받는 메소드 insertTestData
로 분리하였다.
@Before
어노테이션이 적용된 메소드에서 이미 테스트를 위한 user
를 가져오고 있으므로 새로 주입될 boardEntity의 writer에 그대로 사용하였다.
@DisplayName
또한 어떤 테스트인지 알려주기보다 혼란을 더 가중시킬 이상한(..) 문장이었기에 바꿔주었다.
리펙토링된 테스트 코드는 한 눈에 어떤 것을 테스트하려는지 알 수 있다.
❗️글을 작성하는 와중에
TIME
이라는 상수명이 너무 모호하단 것을 깨달았다.insertTestData
라는 메소드를 굳이 들여다보지 않고도 왜 이 값을 파라미터로 받는 것인지 알 수 있게 하는 것이 낫다는 생각이 들었고,SEARCH_KEYWORD
라는 이름으로 변경하였다.DisplayName도 바꿔 주었다. 문맥이 정확하지 않고 제대로 정보를 전달하고 있지 않다는 느낌이 들어 정비해주었다. (🤔주석(엄밀히 말하면 이는 주석은 아니지만 "정보 전달"이라는 부분에서 주석과 유사한 역할을 하기에)은 제대로된 정보를 전달하지 않을 바에는 작성하지 않는게 났다는 말이 떠올랐다.)
boardMapper
(테스트 대상)을 이용하여 키워드로 검색할 경우 총 몇 개의 데이터가 검색되는지 가져와 totalCount
에 저장한다.다른 테스트 코드도 이런 식으로 고치도록 하자.
테스트 코드를 고치다보니 구조가 같은 메소드를 찾을 수 있었다. SearchType
만 변경되기 때문에 각각의 테스트 메소드가 가지고 있던 데이터 주입 메소드인 insertXXXTestData( .. )
들을 하나로 합치도록 하였다.
먼저, 합치기 전의 메소드이다.
for
루프에 사소한 차이가 있을 뿐이다. 분리하고 보니 중복되는 요소를 한 눈에 알아 볼 수 있었다.
다음은 위 세개의 메소드를 하나의 메소드로 합친 것이다.
for
문외에 if .. else ..
문이 들어가면서 더 복잡해 보일 수도 있겠지만 뜯어보면 딱히 그렇지도 않다. 만약 여기서 SearchType이 추가된다면 다시 분리하는...🤔 것이 좋을지도 모르겠다.
전체 코드는 github에서 확인 할 수 있습니다.