본문 바로가기
카테고리 없음

테스트 코드를 어느정도 범위까지 작성해야 하는가.

by 손너잘 2021. 8. 18.

테스트 코드에 대하여 처음 배우고, TDD를 적용하면서 그 위력에 정말 감탄했었다.

처음에는 간단한 자바 프로그래밍을 진행하면서 유닛테스트만 진행하였고, 점점 서비스의 크기를 키우가면서 슬라이드 테스트, 통합테스트, 인수테스트까지 테스트의 범위를 넓혀갔다. 

하지만 여러 테스트의 종류를 접하면서 과연 테스트를 어느 범위까지 작성해야 하는가에 대한 고민을 많이 했었는데 이에대한 개인적인 생각을 작성해 보고자 한다.

개인적으로 필자가 생각하는 필요한 테스트는 아래 4가지 이다.

 

1. 도메인 유닛 테스트

2. 서비스 통합 테스트

3. 서비스 목 테스트 (행위 기반 테스트)

4. 인수테스트

 

지금부터 각 테스트가 필요하다고 생각하는 이유를 하나씩 설명해 보도록 하겠다.

 

1. 도메인 유닛 테스트

백번 말해도 안아깝다. 도메인 유닛테스트는 필수적이다. 도메인은 웹 어플리케이션에서 "도메인"을 그대로 사상하는 가장 중요한 로직이다. 그래서 비즈니스 로직이라고 불리운다.

무슨말인고 하니 우리가 서비스를 만들때에는 항상 "왜"가 따라붙는다. 즉, 요구사항이 존재한다는 이야기다. 그리고 이 요구사항은 프로그래머로 인해 탄생되지 않는다. 기획자, 클라이언트에 의해 탄생된다. 그러면 그들이 원하는 요구사항은 프로그래머에게 전달되고, 그 요구사항(글)을 그대로 코드를 옮기는게 프로그래머의 역할이다.

즉 요구사항은 클라이언트에게 있어서 자신이 원하는 기능을 표현한 것이고, 이 클라이언트의 로직(글)이 프로그래머의 로직(코드)과 달라서는 안된다. 그래서 도메인 로직이 중요하며 모든 테스트를 작성하여 비즈니스 로직이 정확하게 글에서 코드로 옮겨졌는지 테스트 해야 한다.

그 뿐만이 아니다. 유닛테스트를 진행하면 자연적으로 객체지향적 설계로 유도된다. 만일 테스트가 불가능하다고 생각되는 코드가 있다면, 테스트 하고자 하는 객체의 결합도와 응집도를 생각해 보게 된다. 좋은 설계였다면 서로 다른 객체에 서로 다른 책임이 부여되었을 것이고(SRP) 작은 책임 하나하나 테스트가 가능했을 것이다.

 

또한 유닛테스트는 각 클래스의 사용법을 명시하는 "문서"의 역할도 한다. 따라서 깔끔한 가독성을 보여주는것 또한 중요하다.

 

2. 서비스 통합테스트

서비스는 웹 어플리케이션에서 흐름제어를 담당하는, 말 그대로 "서비스" 를 관통하는 레이어다. 비즈니스 로직(도메인)을 조합하여 사용자가 원하는 기능을 만들어낸다. 이 로직을 테스트하는것에 대한 중요성은 말하지 않아도 중요할 것이다.

웹 레이어에서 서비스는 인프라스트럭쳐 레이어와 관련이 깊다. 물론 Presentation - Application - Domain - Infrastructure 계층구조로 봤을 때 Domain이 Infrastructure와 더 가까워서 관련이 깊어보이지만, 일반적으로 DB등 서비스 로직과 밀접하게 관련이 있는 외부 모듈은 Service레이어에서 호출한다(물론, 정석적으로 보자면 Domain에 있는 인터페이스를 실행하는것이기 때문이 서비스레이어는 실제 구현체를 모른다! 따라서 이것의 구현체가 외부 모듈인지 내부 클래스인지 알 수 없다! 라는 개념으로 동작하지만, 우리는 전지적 작가시점으로 이것이 외부 DB라는걸 알기에..). 따라서 서비스에 대한 통합테스트는 필수적이다. 외부 모듈과의 연동은 잘 되는지, 내가 삽입한 input에 대해 기대하는 output이 잘 나오는지 확인하여 실제 prod환경에서 외부 모듈과 연동했을 때 발생할 버그 가능성을 최대한 낮추는 것은 중요하다. 만일 외부 모듈을 Presentation에서 호출하고 있다면... 설계를 다시한번 살펴보아야 할 것이다..

 

3. 서비스 목 테스트 (행위 기반 테스트)

많은 사람들이 서비스 통합테스트가 존재함에도 불구하고 서비스 목 테스트를 왜 해야 하는지 궁금해 할 것이다. 하지만 여기서 필자가 테스트 하고자 하는것은 내가 input을 넣었을 때 기대한 output을 테스트 하고자 하는게 아니다. 실제 외부 모듈을을 몇번 찌르는지, 행위를 테스트 하고자 하는것이다. Mockito 라이브러리에서는 기본적으로 verify를 통해 목 객체가 몇번 호출되었는지 확인 가능하다. 이를 통해 우리는 특정 시나리오에서 외부 모듈을 각각 몇번씩 호출했는지 확인할 수 있다.

이게 뭐가 중요하냐고 물을 수 있다. 하지만 가장 최근 필자의 경험에 따르면 프로젝트의 인터셉터 기능을 리팩토링할 일이 있었는데(참조) 리팩토링을 완료하고 테스트를 돌렸을 때 인수 테스트, 통합 테스트, 유닛테스트를 모두 통과했음에도 불구하고 서비스 목 테스트에서 하나만 fail이 났던 경험이 있다. 에러로그를 보니 특정 인터셉터를 1번 호출하길 기대했는데 2번호출되었다는 에러였다. 확인해보니 리팩토링한 코드에서 잘못된 부분이 있었고 이때문에 특정 인터셉터를 여러번 실행한 것이었다. 다른 테스트에서는 이 버그를 잡아내지 못했다.  그 이유는 인터셉터 체인 순서가 절묘하게 맞아 떨어져 작성한 테스트 케이스가 모두 통과하도록 기댓값이 도출되었기 때문이었다(물론 테스트케이스를 더욱 다양하게 작성하지 못한 이유도 있지만..). 만일 행위기반 테스트가 없었다면 이대로 배포를 진행했을 것이고, 분명 버그를 맞이하고 그 이유를 찾기위해 엄청난 시간을 들였을 것이다. 

 

4. 인수 테스트

인수테스트의 다른 이름을 알고 있는가? 바로 시나리오 테스트다. 실제 사용자의 시나리오를 가정하고 api를 호출하여 기대한 결과값이 도출되는지 확인하는 테스트인 것이다. 이 테스트는 정말정말 중요하다. 실제로 만일 모든 테스트 중에 하나의 테스트만 고르라면 인수테스트만 고를것이다. 인수테스트는 시나리오의 끝에서 끝을 검증하므로 웹 어플리케이션의 모든 레이어를 검증한다. 또한 시나리오 테스트이기 때문에 실제 사용자의 특정 행동에서 버그가 발생하는지 확인할 수 있다. 위 테스트들로는 이러한 것이 불가능하다(만일 로그인 한 후 글쓰기를 누르는 연속적인 행위로 인해 버그가 발생한다면 이를 통합 테스트로 잡아낼 수 있겠는가?).

또한 인수테스트는 서비스 기획자 혹은 클라이언트에게 직접적으로 프로그램이 동작함을 보여줄 수 있는 문서이기도 하다. 따라서 인수테스트는 프로젝트 전체를 관통하는 테스트라고 생각한다.

 

개인적인 사족을 붙히자면 인수테스트는 시나리오가 잘 동작하는지 보여주면 되기 때문에, 코드를 최대한 읽기 쉽게 작성하는게 좋다고 생각한다(물론 모든 코드가 읽기 쉬워야 하지만, 그런 의미보다는 정말 자연어처럼 읽을 수 있어야 한다고 생각한다).

댓글