본문 바로가기

테스트

테스트 코드 개선기 - 테스트 코드에서 얻을 수 있는 인사이트

테스트 코드 개선기 - given 절을 쉽게 구성해보기

 

이전 글에서 테스트 코드의 given 절을 쉽게 구성하는 방법을 배웠다. 이번 글에서는 필자가 테스트 코드를 작성하면서 알게된 테스트 코드에서 얻을 수 있는 인사이트에 대해서 알아보려고 한다.


1. 테스트 코드는 만능이 아니지만, 리팩터링의 든든한 버팀목이다.

테스트 코드를 작성한다고 해서 모든 버그가 사라지고 기능이 완벽하게 동작한다고 믿는 것은 오해다. 테스트 코드는 만능이 아니며, 우리가 작성한 코드에는 언제나 예상하지 못한 버그가 존재할 수 있다.

 

그럼에도 불구하고, 테스트 코드는 리팩터링의 든든한 버팀목 역할을 한다. 테스트 코드의 가장 큰 장점은 프로덕션 코드를 수정하더라도 기존 기능이 정상적으로 동작한다는 확신을 준다는 점이다.

 

지속적으로 성장 가능한 프로젝트를 만들기 위해서는 끊임없는 리팩터링이 필수다. 이때, 잘 작성된 테스트 코드가 뒷받침되어야만 마음 놓고 코드를 개선하고, 리팩터링의 부담을 줄일 수 있다.


2. 필요한 테스트 케이스만 작성하라.

테스트 케이스를 많이 작성한다해서 안정적인 소프트웨어를 보장할 수 있는 것은 아니다. 테스트 코드는 꼭 필요한 경우에만 작성하라.

 

테스트 코드를 처음 접했을 때, 모든 메서드에 테스트 코드를 작성한 경험이 있다. 당시에는 이렇게 하면 소프트웨어가 더 안정적일 것이라고 믿었다. 하지만 코드 베이스가 커질수록 테스트 코드 역시 수정해야 할 일이 점점 많아졌다. 극단적으로 getter나 setter까지 테스트하는 대신 비즈니스 로직 위주로 테스트를 작성하는 것이 효율적이다.

 

만약 시간이 부족하다면, 프로젝트에서 가장 중요한 핵심 가치라도 테스트해야 한다.

 

테스트 코드는 프로덕션 코드처럼 유지보수가 필요한 코드다. 무의미한 테스트 케이스가 많아질수록 유지보수해야 할 코드의 양도 증가한다. 테스트 코드는 자산이 아니라 부채가 될 수 있음을 명심하자.


3. 하나의 클래스에서 많은 테이스 케이스가 존재하면 책임 분리가 필요하다는 신호일 수 있다.

예를 들어 서비스 클래스의 테스트 케이스가 많다면 이는 서비스 클래스에 많은 책임이 부여되었음을 나타낼 수 있다. 서비스 계층에서 검증, DB조회, 매핑 등 다양한 역할을 수행하고 있다면, 비즈니스 로직 테스트 외에도 검증 로직이나 매핑 로직에 대한 테스트가 포함될 가능성이 높다. 이 경우 검증 클래스 매핑 클래스, 도메인 클래스 등을 별도로 만들어 책임 분리를 고려해볼 수 있다.

 

책임 분리를 통해 테스트 코드의 복잡성을 줄일 수 있을 뿐만 아니라, 더욱 객체지향적인 설계를 만들어 낼 수 있다.


4. 모든 비즈니스 로직을  서비스 레이어의 통합테스트로 작성할 필요가 없다.

2, 3번과 연관되어있는 내용이다.

 

테스트 코드는 자산이 아니라 부채라고 했다. 비즈니스 로직이라고 서비스 레이어의 통합테스트로 코드를 작성하게 되면 테스트하는 시간은 늘어나게 된다.

다음 예시를 보자.

    class OrderServiceTest {

        @Test
        void 주문성공() {
    		...
        }

        @Test
        void 한번에_하나의_주문만_가능하다() {
        	...
        }

        @Test 
        void 최대_5개까지_주문할_수_있다() {
    		...
        }

        @Test 
        void 동일한_상품을_중복으로_주문할_수_없다() {
    		...
        }
    }

 

서비스 레이어의 테스트에는 5개의 테스트 케이스가 존재한다. 5개의 테스트 케이스 모두 다 비즈니스적으로 중요한 테스트들이다. 하지만 5개의 테스트를 서비스 클래스의 테스트에 작성할 필요가 있을까? 검증 클래스, 도메인 클래스로 책임 분리를 통해 객체지향 설계를 만들어 내고 통합테스트 대신 단위테스트를 통해 테스트를 실행하는 시간 또한 줄일 수 있다.

    class OrderServiceTest {

        @Test
        void 주문성공() {
    		...
        }
    }
    
    class OrderValidatorTest {

        @Test
        void 한번에_하나의_주문만_가능하다() {
        	...
        }

        @Test 
        void 최대_5개까지_주문할_수_있다() {
    		...
        }

        @Test 
        void 동일한_상품을_중복으로_주문할_수_없다() {
    		...
        }
    }

 

OrderServiceTest 는 통합테스트로 진행하고 OrderValidatorTest 는 단위테스트로 진행하여 개선할 수 있다.


5. 가정이 잘못되면 테스트는 실패한다.

1번에서 언급했듯이 테스트는 만능이 아니다. 테스트 코드의 한계를 지적하는 사람들은 "테스트가 모든 경우를 커버할 수 없다"는 점을 근거로 제시한다. 특히, 기능을 개발한 개발자가 직접 테스트 코드를 작성하면 편향된 시각으로 인해 일부 문제를 놓칠 가능성이 있다.

 

우리는 종종 혼자 버그를 찾으려 머리를 싸매지만 실패하는 경우를 경험한다. 그런데 동료 개발자가 잠깐 살펴봐 주는 것만으로도 문제를 쉽게 해결한 적이 있지 않은가? 이는 우리가 미쳐 보지 못한 부분을 다른 시각에서 봐주었기 때문이다. 이런 이유로, 테스트 코드 작성이 개발자의 시각에 갇혀 있다는 회의적인 의견도 존재한다.

 

그러나 이러한 문제는 개인이 아닌 팀의 협업 방식 또는 테스트 가정의 정확성 문제일 수 있다. 특히 given 절을 구성할 때 잘못된 가정을 바탕으로 데이터를 준비하면 테스트가 실패하거나, 심지어 통과하더라도 실제 버그를 놓칠 수 있다.