독서일기

[Effective Unit Testing] 2장 - 좋은 테스트란?

Woonys 2022. 11. 21. 23:02
반응형

Ch.2 좋은 테스트란?

무엇이 테스트를 좋게 만드는 걸까? 아래와 같은 고려사항이 있다.

  • 테스트 코드의 가독성과 유지보수성
  • 프로젝트 안에서, 그리고 소스 파일 안에서 코드는 적절히 구조화되어 있는가?
  • 테스트가 무엇을 검사하는가?
  • 테스트는 안정적이고 반복 가능한가?
  • 테스트가 테스트 더블을 잘 활용하는가?

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

현업에서 머리를 쥐어뜯게 하는 코드는 모두 접해봤을 것이다. 읽기 어려운 코드는 이해하는 데만 해도 너무 많은 에너지가 소비되기 때문에 유지보수하기가 녹록치 않다. 읽기 어려운 코드일수록 결함 수가 많다.  

 

자동화된 테스트는 결함을 효과적으로 막아주지만, 테스트 역시 코드인지라 가독성 문제에서 벗어날 수 없다. 읽기 어려운 코드는 검증하기도 어렵다. 결과적으로 테스트를 조금만 작성하는 사태로 이어진다.  

 

아래 다듬어지지 않은 난해한 테스트 코드를 하나 살펴보자.

@Test
public void flatten() throws Exception {
    Env e = Env.getInstance();
    Structure k = e.newStructure();
    Structure v = e.newStructure();
    // int n = 10;
    int n = 10000;
    for (int i = 0; i < n; ++i) {
        k.append(e.newFixnum(i));
        v.append(e.newFixnum(i));
    }
    Structure t = (Structure) k.zip(e.getCurrentContext(),
    new IObject[] {v}, Block.NULL_BLOCK);
    v = (Structure) t.flatten(e.getCurrentContext());
    assertNotNull(v);
    }

위의 테스트는 무엇을 검사하려는 것일까? 여기서 무슨 일이 벌어지고 있는지 설명할 수 있나? 이는 가독성이라는 문제에서 비롯된다.

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

구조화해두면 좋다는 점뿐만 아니라 제대로 구조화하지 않으면 역으로 피해를 본다.  

 

하지만 아무 구조나 다 코드를 이해하는 데 도움이 되는 건 아니다. 우리 두뇌와 사고 모델이 지지고 볶을 수 있을 정도로 정리된 구조라야 한다. 생각 없이 코드를 여러 클래스, 메소드로 흩뿌려도 한 번에 봐야 할 코드는 줄어든다. 하지만 그것이 잘 이해되는 것과는 전혀 별개의 문제다.  

 

코드가 도메인 모델이나 머릿속의 개념과 맞지 않아서 나누는 명확한 경계를 찾을 수 없다면 차라리 나누지 않느니만 못할 수도 있다. 하나의 개념이 여러 조각으로 나뉜 꼴이 되어 여러 소스 파일을 오가느라 시간이 더 낭비되기 때문이다.  

 

이를 해결하는 구조는 간단하다. 고수준 개념을 구현된 코드에 빠르고 정확하게 대입할 수 있는 구조면 된다.  

 

뜻밖의 문제가 발생하지 않게끔 현재의 구현 상태를 정확히 이해하려 한다. 이때 가장 좋은 방법은 코드 동작 방식을 구체적으로 보여주는 테스트 코드를 정독하는 것이다. 이때 테스트 코드가 제대로 구조화되어 있지 않으면 어디를 봐야하는지 찾아내기 어려울 것이다.  

 

결국은 읽기 쉽고, 찾기 쉽고, 이해하기 쉽도록 한 가지 기능에 충실한 테스트가 필요하다. 이렇게 하면 아래와 같은 이점이 생긴다.

  • 현재 작업과 관련된 테스트 클래스를 찾을 수 있다.
  • 그 클래스에서 적절한 테스트 메소드를 고를 수 있다.
  • 그 메소드에서 사용하는 객체의 생명 주기를 이해할 수 있다.

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

코드를 분석할 때는 가장 먼저 전체 테스트를 돌려보고 잘 동작하는 부분과 문제가 있는 부분을 확인한다. 이때 가끔은 테스트의 이름을 너무 믿어버리는 실수를 저지른다. 보통은 테스트의 이름을 보면 해당 테스트가 검사하는 내용을 알 수 이쓴데, 실제로는 이름과 전혀 관련 없는 것을 검사하는 경우가 종종 있다.  

 

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

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

테스트 코드에서 테스트가 얼마나 독립적인가를 잘 살펴봐야 한다. 이때 아키텍쳐의 경계 부분이 특히 중요하다. 경계에서 일어나는 일을 관찰하면서 수많은 코드 냄새를 맡을 수 있는데, 아래 요소와 관련이 있다면 각별한 주의가 필요하다.

  • 시간(Time)
  • 임의성(Randomness)
  • 동시성(Concurrency)
  • 인프라(Infra)
  • 기존 데이터(Pre-existing Data)
  • 영속성(Persistency)
  • 네트워킹(Networking)

격리와 독립성이 중요한 이유는 그것이 없다면 테스트를 실행하고 관리하기가 훨씬 어렵기 때문이다. 단위 테스트를 실행하기 위해 개발자가 해야 할 귀찮은 작업이 훨씬 많아진다. 파일 시스템의 특정 위치에 빈 디렉터리를 만들어둬야 하거나, MySQL 버전과 포트 번호를 확인하거나, 테스트가 사용할 로그인 정보를 데이터베이스에 미리 추가하거나, 환경 변수를 잔뜩 설정하는 일 등이 여기에 해당한다. 이 모두는 개발자가 하지 않아도 됐을 작업이다. 이런 작은 하나하나가 모두 작업량을 늘리고 기묘한 테스트 실패를 일으키는 원인이 된다.

같은 테스트 클래스에 속한 테스트 메소드끼리도 순서를 가정하면 안 된다.
테스트를 서로 종속되지 않게 하라는 조언은 클래스끼리 뿐만 아니라 한 클래스 내 서로 다른 메소드 간에도 마찬가지여야 한다.

 

가끔 테스트 전체를 실행할 때는 문제없다가 하나만 따로 실행하면 실패하는 경우가 있다. 이는 상호의존성이 있다는 강력한 징조다. 반드시 피해야 한다.

 

다시 말하면, 시간, 임의성, 동시성, 인프라, 영속성, 기존 데이터, 네트워킹 관련 코드를 검사할 때는 특별히 더 신경써야 한다. 이들과의 종속성은 가능하면 피하는 게 최선이고, 아니면 작고 격리된 단위로 구분해 다른 테스트를 보호해야 한다.

 

실전에서는 어떻게 할까? 아래와 같은 방법을 시도해볼 수 있다.

  • 테스트 더블로 서드파티 라이브러리와의 종속성을 제거한다. 손수 만든 어댑터로 적절히 감싸주는 것이다. 성가신 부분이 어댑터 안으로 감춰지므로 나머지 애플리케이션 로직과 분리해서 검사할 수 있다.
  • 테스트에 필요한 자원을 테스트 코드와 같은 위치에 둔다. 자바 프로젝트라면 같은 패키지에 두면 된다.
  • 테스트가 사용할 자원을 직접 만들도록 한다.
  • 테스트가 필요한 문맥을 직접 설정하게 한다. 절대 다른 테스트에 의존하지 말자.
  • 영속성이 필요한 통합 테스트라면 인메모리 데이터베이스를 할용한다. 테스트를 위한 깨끗한 데이터를 훨씬 간단하게 준비할 수 있다.
  • 스레드를 사용하는 코드는 동기식과 비동기식을 구분 지어서 골치 아픈 동시성 문제는 소규모의 전문 테스트 그룹에 맡긴다. 평범한 동기식 코드로 작성된 나머지 로직 대부분은 별다른 어려움 없이 검사할 수 있게 된다.

2.5 믿음직한 테스트라야 기댈 수 있다.

가장 뒷목잡는 테스트는 아무것도 검사하지 않는 테스트다.

아래와 같은 사례는 try-catch로 감싸기 좋아하는 프로그래머가 짠 코드다. 이런 코드는 골칫거리다.

@Test
public void shouldRefuseNegativeEntries() {
    int total = recode.total();
    try {
        record.add(-1);

    } catch (IllegalArgumentException expected) {
        assertEquals(total, record.total());
    }
}

이런 테스트는 절대 실패하지 않는다. 심지어 에외를 던져도 실패하지 않는다. 실패하지 않는 테스트는 있으나 마나다.

  

테스트를 믿고 의지하려면 반복할 수 있게 만들어야 한다. 열 번 실행하면 열 번 모두 반드시 같은 결과가 나와야 한다. 그렇지 않으면 빌드할 때마다 테스트 결과를 직접 중재해줘야 한다.  

  

예컨대 모든 테스트가 최소 한 번씩은 성공할 때까지 네다섯 번을 반복해서 실행하는 행위는 맞는 것일까? 그런데 만약 테스트 코드에 난수 발생기는 물론 시간 관련 API- 심지어 비동기 - 까지 사용한다면 얘기가 달라질 수 있다.  

 

애플리케이션이 비동기적 요소나 현재 시간에 종속된 코드를 포함한다면 그 부분을 인터페이스로 감싸 격리해야 한다. 그렇게 하면 테스트 더블로 대체할 수 있어 반복 가능한 테스트를 만들 수 있다.

2.6 모든 일이 그렇듯 테스트에도 도구가 쓰인다.

위에서 계속 언급하는 테스트 더블이란 무엇인가? 테스트 더블이란 우리가 흔히 말하는 Stub, Mock 객체 등으로 알고 있는 개념을 통칭하는 용어다. 근본적으로는 테스트를 위해 실제 구현체와 교체할 수 있는 객체를 말한다.  

 

테스트 더블은 프로그래머의 최고의 친구다. 아래는 그 장점 중 일부다.

  • 원래의 로직을 간소화된 코드로 대체하여 테스트 속도를 개선한다.
  • 만들어내기 어려운 특수한 상황을 시뮬레이션한다.
  • 대상 객체의 내부 상태나 동작 등 테스트가 접근할 수 없던 정보를 얻어낸다.

테스트 더블보다 훨씬 필수적인 도구라면 JUnit과 같은 테스트 프레임워크를 꼽을 수 있다. 프레임워크를 이용하면 반복할 수 있고 자동화된 테스트를 만들기 훨씬 수월하다.  

 

테스트 프레임워크, 테스트 더블과 함께 자동화된 테스트 작성에 필요한 세 번째 도구는 바로 빌드 도구이다. 어떤 빌드 프로세스를 따르건, 자동화된 테스트를 통합하지 않는 빌드는 결코 용납될 수 없다.

2.7 요약

2장에서는 좋은 테스트를 만드는 요소를 큰 그림에서 정리해봤다.

  • 먼저, 테스트의 필수 미덕 중 하나인 가독성을 얘기했다. 아무리 애를 써도 이해할 수 없는 테스트 코드는 그 자체가 곧 큰 골칫거리가 되어 유지보수를 어렵게 만든다.
  • 테스트의 구조가 유용한 테스트 제작에 중요한 이유를 살펴봤다. 잘 구조화된 테스트라면 원하는 코드를 빠르게 찾을 수 있고 논리 흐름도 쉽게 이해할 수 있다.
  • 엉뚱한 걸 검사하는 테스트 및 그로 인한 문제를 살펴봤다. 개발자를 잘못된 길로 안내하거나 본질을 흐려버려서 테스트가 의도했던 진짜 논리를 감추고 가독성을 떨어뜨리는 결과를 낳는다.
  • 가끔씩 의심스러운 동작을 하는 테스트도 문제였다. 이런 불신을 키우는 공통 원인을 찾아내고 반복할 수 있게 만드는 것이 테스트에 얼마나 중요한가 강조했다.
  • 마지막으로, 자동화된 테스트 작성을 보조해주는 필수 도구 세 가지(프레임워크, 테스트 더블, 빌드 도구)를 소개했다.
반응형