Unit Test (단위 테스트) 도입하기 - 2편
- 원문 : https://gregor77.github.io/2019/08/16/about-unit-test/
- 관련글 : https://www.popit.kr/unit-test-단위-테스트-도입하기-1편/
이전글에서 단위 테스트의 장점과 도입하기 전에 궁금증에 대해서 알아봤다. 이제 단위테스트를 도입할 준비가 되었다면 이번글은 단위테스트 시작 시에 참조할 만한 작성 기준과 지속하기 위한 방법에 대해서 알아보겠다.
꼭 이렇게 할 필요는 없으며, 가장 중요한 것은 팀원 모두가 합의한 방식으로 시작하면 되고, 지속하는 과정중에 불편한 점이 있으면 언제든지 개선해서 적용하는 과정을 반복한다면 팀에 단위테스트 문화를 성공적으로 정착시킬 수 있을 것이다
시작하기
단위테스트의 장점도 알았고, 도입하기 전에 가지고 있는 두려움도 어떻게 극복할 수 있을지 이야기해보았다. 그렇다면 당장 어떻게 팀에 도입할 수 있을까? 일단 팀원 모두가 공통적으로 필요성을 공감해야 지속할 수 있다. 기존에 프로젝트에 어떤 불편한점이 있었고, 단위테스트를 도입함으로써 어떻게 해결할 수 있을지 공감하고, 필요성을 느끼는 상태에서 시작해야 오래갈 수 있다.
팀원 중 한명이라도 필요성을 못 느끼거나, 부정적인 생각을 가지고 있다면, 충분히 대화를 통해서 동의를 구하고 시작하자. 그렇지 않다면 그 사람은 테스트케이스 짜기를 앞으로도 꺼려해 누군가가 커버되지 못하는 곳을 채우는 상황들이 발생할 수 있기 때문이다. 기존에 작성하지 않는 팀이 도입하기 위해서는 어떤 변화가 필요할까?
PM은 테스트케이스를 도입 초반에는 팀의 Velocity가 떨어질 수 있다는 것을 인지해야 한다.
- 팀의 Velocity가 올라올때 까지 얼마나 걸릴지 확인해야하고, 그것을 감안해 일의 우선 순의를 정리할 수 있어야 한다.
- 팀원들이 테스트케이스 작성에 익숙해지면 그 이후부터는 당연한 개발 과정일 뿐이지 추가적인 비용이 들지 않는다. 즉, Velocity가 떨어지는 등의 일은 발생하지 않을 것이다.
테스트케이스 작성의 원칙과 범위, 작성 방법을 정하고, 변경되면 팀원과 공유한다.
- 개발할때 유지보수를 위하여 개발자마다 개발 스타일이 다 다를지라도, 표준을 정하고, 코드 컨벤션을 포함한 스타일을 맞추는 작업을 진행한다. 테스트도 동일하게 필요하다.
- 테스트케이스의 유지보수를 위해서 얼마나 간결하게 작성을 할 것인지 범위를 정하는 것과 어떤 assertion이나 matcher 도구 등을 사용할지 정하고 가야 작성법이 단순해지고, 유지보수가 편하다. (예: junit + hamcrest를 사용할지, assertJ를 사용할지)
- 테스트케이스 작성법이나 원칙은 계속 개선해나가는 것이 좋다. 단 변경될마다 팀원들과 적극적으로 공유해야 테스트케이스 작성을 비슷한 수준으로 유지할 수 있다.
- 작성법이 서로 다르거나, 테스트케이스를 커버하는 범위가 서로 다르면 유지보수가 어렵다.
단위테스트 작성방법
"하나의 테스트케이스에 최소한의 기능만 검증하고, 최대한 간결하게 작성한다."
- 테스트케이스가 많은 것은 큰 문제가 되지 않는다. IDE 툴을 사용하면 테스트케이스가 많더라도 쉽게 찾고, 편집이 가능하다.
- [Anti Pattern] Java 테스트케이스 작성시, 클래스에 하나의 메소드를 하나의 테스트케이스 함수로 작성하는 경우를 본적이 있다. 유지보수가 하기 편하게 하기 위해서 기준을 그렇게 잡았다고 한다.
- 비지니스 로직이 없는 경우는 충분히 가능하다.
- 하지만 조건문, 반복문, 분기문이 있으면 하나의 메소드라 할지라도, 검증 대상은 여러 상황이 있는 것이다. 이를 하나의 테스트케이스 함수에서 검증하는 것은 너무 어렵다.
- 일단 하나의 테스트케이스 함수내에서 여러 상황이 존재할 것이고, 여러 상황을 의도한대로 동작하게 위해서 Mock객체에 Stub하는 과정을 할 것이다. 일단, 테스트케이스의 수직거리가 너무 길 것이고, 중간에 에러가 발생하면 어떤 상황에서 실패했는지 살펴보는데 추가적으로 시간이 든다. 이는 테스트를 유지보수하기에 비효율적인 방법이다.
- 소스를 변경해서 테스트케이스를 고치야 하는 경우, 테스트케이스를 이해하기위해서 시간을 많이 든다면 얼마나 비효율적인가? 유지보수하기 편하게 작성하는 것이 중요하다.
"입력값에 대한 결과 값을 검증하는 방식으로 작성하라."
- 기본적으로는 입력값에 대한 결과값을 확인하는 방식으로 작성하면 소스가 변경되더라도 테스트케이스를 변경할 일이 훨씬 적다.
- 구현체에 의존하지 않는 테스트케이스를 작성해야 유지보수가 쉽다.
- 구현체가 변경되더라도, 테스트 케이스를 변경할 일이 적다.
- 단위테스트는 Mock객체를 사용하고 때로는 결과를 의도하는대로 Stub도 해야되기 때문에, 어느정도 구현체에 의존적일 수 밖에 없다. 이런 경우를 제외하고는 최대한 덜 의존적으로 작성하라.
- Java에서 단위 테스트 작성시 Private이나 Static method에 Stub을 하기 위해 PowerMock을 사용하는 경우가 있다. File 관련 테스트를 작성하는 경우에는 PowerMock이 도움이 된다.
- 입력값에 따른 결과값을 확인하는 방식으로 테스트를 작성할 수 있음에도 불구하고, PowerMock으로 작성하면 구현체에 의존적인 테스트가 되고, 구현체가 변경되면 별도의 유지보수 노력이 들기 때문에 비효율적이다.
"불안한 부분이 없도록, 개발하는 부분은 최대한 커버하라."
- 우리가 개발하는 부분은 최대한 꼼꼼하게 작성해야 테스트케이스의 효과를 최대한 얻을 수 있다. 촘촘한 그물로 물고기를 많이 잡을 수 있는 것과 비슷하다.
- 커버리지 수치는 꼼꼼하게 단위테스트를 작성하면 따라온다. 결코 커버리지 수치 자체가 목적이 되어서는 안된다.수치가 목적이 되는순간 의미없는 테스트를 작성하고, 단위테스트를 통해서 얻고자 하는 효과가 적어진다.
"Third Party Library의 기능은 믿고, 단위테스트 검증대상에서 제외하라."
- 효율적인 개발을 위해 적용한 라이브러리 또는 플러그인의 기능은 정상적으로 동작할거라 믿고 단위테스트 검증 대상에서 제외한다.
- 그럼에도 외부 라이브러리를 사용하는 대상이 중요한 기능이고 테스트가 없어서 커버하지 못하는 상황이면, Unit Test외에 Integration Test나 다른 테스트로 검증을 하는 것도 고려해본다.
- 예를들어, Spring에서 JPA, Mybatis 등을 사용하는데 Query의 기능을 검증하고 싶은 경우가 있다. 단위테스트를 위해 라이브러리 내부에서 동작하는 방식을 확인하면서 Mock객체로 바꾸고, Stub하는 과정은 상당히 괴롭고, 유지보수 비용이 많이 든다.
- 이런 경우에는 Embedded DB나 Test DB를 별도로 두고, 검증 대상을 실행해서 입력값에 따른 결과값을 체크하는 방식으로 Repository Test 또는 Integration Test로 커버하는 것이 더 효율적이다.
단위테스트로 커버하는 범위와 테스트케이스 작성원칙을 팀에서 결정했고, 드디어 서비스 개발시 단위테스트를 작성하기 시작했다. 시작이 반이라고는 하지만 앞으로 지속하는 것이 진짜 중요하다. 어떻게하면 고생해서 시작한 단위 테스트의 장점을 만끽하면서 지속할 수 있을까?
단위테스트를 지속하는 방법
테스트케이스를 작성하면서 개발을 시작했는데, 코드 변경이 없어서 테스트케이스를 왜 해야하는지 모르겠다고 한다. 그냥 일만 두배로 하는 기분이고 테스트케이스 작성이 어떤 장점이 있는지 모르겠다는 의견을 회고에서 들었다. 한번 작성한 코드를 개선하는 노력을 들이지 않고, 정상적으로 동작하니까 소스를 그대로 둔다. 이런 팀 분위기 속에서 어떻게 단위테스트를 지속할 수 있을까?
Refactoring(리팩토링)은 사랑입니다.
개인적으로는 작성한 소스는 생명체라 생각하는 편이다. 더 이상 관심을 두지않고, 그대로 둔다면 그냥 죽어있는 상태다. 이런 상태에서 변경이 있을때마다 소스는 누더기처럼 덕지덕지 늘어나고, 나중에는 변경하기 힘들어서 기존에 만들어진 구조에서 최소한의 변경만 하려고 한다.
이런 상황을 포장해서 고급스럽게(?) 레거시 프로그램이기 때문에 어쩔 수 없다고 말하는 사람들을 만나면 복잡한 생각이 든다. 스스로 일하기 어려운 상황을 만들고 그 안에서 힘들게 일하고 있기 때문이다.
리팩토링의 장점은 정말 크다고 생각한다. 내가 하고 있는 일, 내가 만드는 것들에 대해 가치를 불어넣어주는 중요한 행위라 생각한다. 리팩토링이 성공했다는 보장은 기존의 테스트케이스가 통과하는 것이다. 테스트케이스가 꼼꼼하게 작성되어 있을수록 더 나은 코드로 개선할 수 있는 기회가 많고, 안심하고 변경이 가능하다.
- 유저스토리를 하나하면 리팩토링을 하나한다.
- 기존에 단위 테스트케이스가 주는 효과를 경험하지 못한분들과 페어 프로그래밍을 할 때는 유저스토리를 하나하면 리팩토링을 하나하는 순으로 일을 하는 편이다.
- 우리가 만든 코드를 개선하면서 바로 작성한 테스트케이스가 효과가 있는 것을 경험하는 과정이 말보다 훨씬 더이해하기 좋은 방법이기 때문이다.
- 코드리뷰를 시작해보라.
- 개인적으로는 페어프로그래밍을 하면 코드리뷰가 불 필요하다고 생각한다. 페어를 돌면서 소스가 공유가 되기 때문에 코드리뷰보다 더 극단적으로 코드공유가 가능하기 때문이다.
- 코드리뷰를 통해서 더 나은 코드를 보고, 내 코드가 개선될 수 있다는 것을 알게되면 지속적으로 리팩토링을 할 수 있다.
- 페어프로그래밍을 현실적으로 하기 어려운 곳이 있다면 코드리뷰를 시작해보라.
테스트 자동화는 필수 !!
테스트 자동화는 딜리버리 가능한 제품을 만들기 위해서는 선택이 아니라 필수다. 테스트 커버리지 측정 및 Report 생성해주는 라이브러리들, Sonar, Build Monitor를 조합하면우리가 만들고 있는 제품의 상태를 눈으로 확인할 수 있다. 팀에서 지켜지는 코드품질의 보이지 않는 선이 정해진 상황에서 이런 시각적인 툴의 도움을 받으면 항상 현재 수준을 지키기 위해서 노력한다.
빌드 모니터의 목적은 눈으로 문제를 바로 확인함으로써 가장 빠르게 대처할 수 있다는 것이다. 빌드 모니터의 대상은 테스트 커버리지나 코드 스멜, 보안성 취약점등이며 상태를 항상 눈으로 확인할 수 있다. "깨진 유리창의 법칙"을 들어봤을 것이다. 그게 팀에 동일하게 적용된다.
수치 자체가 목적이 되어서는 안된다. 하지만 팀에서 정해지는 코드품질이 어느 정도의 수치로 표현되면, 수치가 낮을때는 아무도 테스트 커버리지나 코드스멜, 보안성 취약점 수준이 낮더라도 신경쓰지 않는다. 하지만 우리 현재 코드 품질이 지켜지고 있는 상태에서 배포를 통해 기준이 떨어지게되면 팀원 누구나 자발적으로 지키기 위해 노력하는 것을 확인할 수 있었다.
마치며
이번 글 "Unit Test (단위 테스트) 도입하기"는 사내에서 프로젝트를 수행하며, 개발 Practice 중에 TDD와 단위테스트 전파했던 경험에 기반한 내용입니다. 개인적인 경험에 기반한 내용이라 부족하거나, 혹시 틀린 내용이 있을 수도 있습니다. 현재 상황에 맞추어 참고하시면 될 것이라 생각합니다.
예전에 프로젝트를 진행할때 단위테스트, TDD를 적용하고 싶은데 방법을 몰라서 시작을 못하거나, 적용은 했지만 경험이 없어서 지속하지 못한 적이 많았습니다. 그 이후에 어떻게 작성하는지 기준과 지속하는 방법을 알고 싶어서 지금 부서에 와서 일을 하게 되었고, 3년이 지난 시점에 이 글을 쓰게 되었습니다. 이번 글을 통해서 저와 비슷한 상황을 겪는 분들께 도움이 되었으면 합니다.
지금은 일할때 커버되지 않는 부분이 있다고 생각이 들면 더 많은 테스트케이스를 짜려고 합니다. 단위테스트로 부족하다고 생각이 들면 다른 테스트를 통해서 커버할 수 있는 방법이 있는지 고민합니다. 서비스를 만들다보면 소스가 자주 변화는 경우도 많고, 오늘의 나는 어제의 내가 만든 소스를 보면서 이해하지 못하는 순간이 많습니다. 그런 상황에서 믿을 것은 동료도 있지만 꼼꼼하게 작성한 테스트케이스가 크게 의지가 됩니다.
사람은 한번 편안한 것을 알게되면 불편한 때로 돌아가기가 어렵습니다. 저에게 단위테스트가 그랬습니다. 지금이라도 단위테스트를 어려워 하는 분들이 계시다면 이 글을 통해서 오해가 많이 풀리고, 시작하는 계기가 되었으면 합니다. 만약에 궁금한 점이 있으시면 댓글로 남겨주세요. 확인하는대로 답변드리겠습니다.