본문 바로가기
가치관 쌓기/개발 돌아보기

좋은 테스트에 대해서 이야기 하기

by simplify-len 2022. 9. 18.

좋은 테스트에 대해서 이야기해볼까 합니다. 여기서 '좋은' 이라는 말은 개발자에게 언제나 고민의 시작이지 않았을까 싶습니다.

직접 테스트 코드를 작성하며 이 책(이펙티브 유닛 테스팅)에서 말하는 좋은 테스트에 대해서 공감이 되는 부분이 몃년이 지나서야 드디어 조금씩 알게되는 것 같습니다.

다음과 같은 주제로 이야기해보려 합니다.

_2.1 읽기 쉬운 코드가 유지보수도 쉽다.
_2.2 구조화가 잘 되어 있다면 이해하기 쉽다
_2.3 엉뚱한 걸 검사하는 건 좋지 않다
_2.4 독립적인 테스트는 혼자서도 잘 실행된다

 

읽기 쉬운 코드가 유지보수도 쉽다.

여기서 말하는 읽기 쉬운 코드란 어떤 코드를 말하는 걸까요? 그 가독성은 어떻게 채울 수 있을까요? 가독성 높은 코드? 가독성 이라는 단어는 눈으로는 이해할 수 있지만 코딩하는 손과 말하는 입으로는 설명하기 어려운 부분입니다.

가독성을 높인다. 라는 용어는 코드을 작성하는 개발자에게 주어진 역할은 다음과 같습니다.

  • 짫은 코드
  • 의도가 드러난 변수명/함수명/클래스명 그 외
  • 인터페이스을 활용한 명확한 메세지 전달 코드
  • 형식(Given, When, Then) 지켜진 코드
  • 군더더기가 없는 코드

 

실제로 테스트 코드을 작성하다보면 위와 같은 내역을 지키기 어렵습니다.

    @Test
    void notNull() {
        assertThat(sut).isNotNull();
    }

    @Test
    void refund() {
        RefundContext build = mock(RefundContext.class);

        given(finder.findByOrderId(ANY_ORDER_ID)).willReturn(build);
        given(build.refund(anyInt(), anyString())).willReturn(execution);

        sut.refund(ANY_ORDER_ID, REASON, 1000, new HashMap<>());

        verify(executor).execute(execution);

    }

위 코드는 제가 작성했었던 실제로 운영중인 서비스의 테스트 코드입니다. 무척이나 반성해야될 코드라고 생각합니다.

1. 읽기 쉬운 코드가 유지보수도 쉽다. 를 찾아볼 수 있나요?

불필요한 Mocking, 더군다나 Given, When, Then 도 명확하지 않습니다.

구조화가 잘 되어 있다면 이해하기 쉽다.

구조화란 어떤 의미일까요? 좋은 테스트에서 구조화가 무슨 말일까요? 

이 책에 나오는 다이어그램입니다. 이 부분은 꼭 공유하고 싶은 부분이였습니다.

img

코드가 구조화 되어 있지 않다. > 변경된 개념을 코드에 투영하기 어렵다. > 사소한 변경이라도 악전고투를 유발한다. > 아무도 손대려 하지 않는다. > ??

여기서 ?? 은 작은 조각으로 나눈다. 입니다.

작은 조각으로 나누는게 무슨 의미가 있을까? 그럼 작은 조각이 구조화를 의미하는 걸까? 꼭 그렇지만은 않을 수 있습니다.

작은 조각으로 왜 나누는지에 대해서 고민해봤을까요? 작은 조각으로 나눈 이유는 코드상의(도메인 상의) 경계를 명확하게 하기 위해서입니다.

프로덕트 코드를 이용하는 첫번째 클라이언트인 테스트 코드가 무엇을 테스트해야 하는지? 무엇을 의도하기 위한 용도인지 그 경계를 명확하게 하기 위해서라고 생각합니다.

그 경계가 명확하지 않은 상태에서 무분별한 구조화는 차라리 하지 않느니만 못할 것입니다.

엉뚱한 걸 검사하는 건 좋지 않다

이 책에서 소주제는 진짜 어그러를 잘 끌게 네이밍한게 아닐까 싶어요. 주제만 듣고도 벌써 공감의 마음이 샘솟으니까요.

이 주제에서 엉뚱한 걸 검사한다는 건 여러 의미가 있을 수 있지만, 제가 생각하는 엉뚱한 걸 검사한다 는 것은 테스트가 의도하고자 하는 메세지를 담지 못했다는 것입니다.

    @Test
    void notNull() {
        assertThat(sut).isNull();
    }
// 음? notNull 인데, 테스트 코드에서는 isNull 이네...?

우리는 테스트 코드에 대해서(적어도 테스트 코드를 작성해야 한다고 믿는 사람들에게) 어느 정도의 신뢰를 가지고 있습니다.

분명 어떠한 테스트도 어떤 역할을 해주고 있을 것입니다. 하지만 그것이 전달되지 않는다면 무슨 의미가 있을까요?

이때 가끔은 테스트의 이름을 너무 믿어버리는 실수를 저지르곤 한다. 보통은 테스트의 이름을 보면 그 테스트가 검사하는 내용을 알 수 있는데, 실제로는 이름과 전혀 관련 없는 것을 검사하는 경우가 종종 있다. - 이펙티브 유닛 테스팅 p50

올바른 것을 검사하는 것 못지않게 올바른 것을 똑바로 검사하는 것도 중요하다. 특히 유지보수 관점에서는 어떻게 구현했느냐가 아니라 의도한 대로 구현했느냐를 검사하는 게 중요하다. - 이펙티브 유닛 테스팅 p51

독립적인 테스트는 혼자서도 잘 실행된다

분명 과거에는 알지 못했지만, 누군가의 액션에 의해서 독립적인 테스트는 혼자서도 잘 실행된다 에 대해서 깊이 공감할 수 있었습니다.

테스트 코드가 독립적인 테스트가 될 것이라는 것은 당연한거 아니야? 라 말할 수 있습니다.

근데요. 우리가 만드는 서비스에는 참 많은 의존성이 붙는 것 같습니다. 가장 흔하게 볼 수 있는 것이 바로 데이터베이스이구요, 좀 더 나아가면 AWS SQS,SNS 등의 특정 솔루션 등이 있습니다.

이런거 어떻게 테스트할 수 있을까요?

데이터베이스는 H2 데이터베이스로 하면 되지 않을까? 라고 생각하실 수 있습니다. 그것은 사실 감사하게도 우리가 사용하는 프레임워크(spring boot)의 도움이 있어서 가능했던 아니였을까?

만약 프레임워크가 없었다면 어떻게 테스트 할 수 있을까? 벌써부터 약간의 답답을 느끼지 않는가? 테스트할 때마다 DB를 켜놓을 것인가?

그 외에도 다음과 같은 의존성이 있을 수 있습니다.

  • 시간
  • 임의성
  • 동시성
  • 인프라
  • 기존 데이터
  • 영속성
  • 네트워킹

위와 같은 것은 우리가 제어할 수 없다는 특징을 가집니다. 그러므로 독립적인 테스트를 만들기 어렵게 만드는 것입니다.

하지만, 이런 것을 피할 수 있는 도구를 만들거나, 서드파트를 활용해 테스트가 격리와 독립성이 될 수 있도록 만들어야 합니다.

댓글