테스트

무엇을 테스트 할것인가? 어떻게 테스트 할것인가 - 권용근님

ysk(0soo) 2022. 11. 10. 19:09

스프링캠프 2019 무엇을 테스트할 것인가? 어떻게 테스트할 것인가? (권용근 님)

좋은 테스트와 테스트 하는 방법에 대하여 고민하던 중, 좋은 영상 자료를 받게 되어 내용을 적으면서 공부한 내용입니다.

영상링크

우리가 테스트를 어려워하고, 결국 포기하는 이유는 잘못된 테스트를 작성했을 가능성이 크다고 생각합니다.

그래서 스프링 프레임워크를 사용하면서 우리는 무엇을 테스트해야 하는지, 어떻게 테스트할 것인지를 이야기해보려고 합니다.

 

TDD 보다는 테스트에 대하여 이야기하는 세션입니다. 


목차

  1. 테스트로 얻을 수 있는것
  2. 무엇을 테스트할 것인가?
  3. 어떻게 테스트할 것인가?
  4. TIP & RULE

1. 테스트로 얻을 수 있는것

  1. 마음의 안정성 -> 안정감과 자신감 -> 소프트웨어의 안정감과 자신감

안정감과 자신감을 얻어야 하는 대상은

  1. 현재와 미래의 나
  2. 현재와 미래의 동료

를 위해서 안정감과 자신감을 줘야한다.

오로지 나를 위해서가 아닌, 동료나 미래의 그 프로젝트를 넘겨받을 동료를 생각해보자.

2. 무엇을 테스트할 것인가?

예제로 로또 시스템.

요청이 들어갔을 때 6개의 숫자를 반환하는 시스템.

비즈니스 요구사항 정리

  1. 여섯개의 숫자 반환
  2. 중복되지 않은 숫자
  3. 랜덤하게 반환 -> 요청 때 마다 서로 다른것

간단한 코드 예제

public class LottoNumbersGenerator {

    private static final int LOTTO_TICKET_LIMIT_NUM = 6;

    private final LottoNumberCollection collection;

    public LottoNumbersGenerator(LottoNumberCollection collection) {
        this.collection = collection;
    }

    public List<Integer> generateTicket() {
        Set<Integer> ticket = new HashSet<>();
        List<Integer> lottoNumbers = collection.createNumbers();

        shuffleNum(lottoNumbers);

        for (int i = 0; ticket.size() < LOTTO_TICKET_LIMIT_NUM; i++) {
            ticket.add(lottoNumbers.get(i));
        }

        return new ArrayList<>(ticket);
    }

    private void shuffleNum(List<Integer> lottoNumbers) {
        Collections.shuffle(lottoNumbers);
    }

}

테스트

public class LottoNumbersGeneratorTest {
    @Test
    @DisplayName("6개의 숫자를 반환")
    void generateTicket() {
        LottoNumbersGenerator generator = new LottoNumbersGenerator(new LottoNumberCollection());

        List<Integer> ticket = generator.generateTicket();

        assertThat(ticket.size()).isEqualTo(6);

    }
}

중복되지 않은 숫자 테스트는 안해도 되는가??

  • 구현은 언젠가 변할 수 있다라는 것을 명심해야한다.

자기가 짠 테스트 코드는 자기만 알기 때문에 남이 알지 못한다.

private 메소드인 actD는 어떻게 테스트할 것 인가??

테스트 케이스를 추가해서 actC()를 테스트한다.

설계를 했던 사항 그대로 테스트 코드로 옮겨져서 테스트되야한다.

원칙 1. 우리가 코딩해서 구현한 내용이 아닌 설계를 테스트 해야한다

구현이 아닌 설계를 테스트 해야한다

원칙2. 테스트 가능한 것과 불가능한 것을 살펴보자

메서드의 콜트리를 보면 맨 아래 테스트할 수 없는 메소드가 있다면 전체가 테스트할 수 없게 만들어지고 물들어간다.

 

테스트 할 수 없는 영역이 뭐가있지?

 

Non-Testable - <제어할 수 없는 영역>

  • random 메소드, Shuffle, LocalDate.now()
  • 외부 세계
    • HTTP
    • 외부 저장소

 

랜덤값이나 시간은 우리가 제어할 수 없다.

외부 API나 외부 저장소는 테스트케이스는 만들어지면 고정된 상태이다.

외부에서 무언가 일이일어나면 우리 테스트는 실패하게 된다.

-> 테스트 할 수 없는거를 테스트할려했기때문에 테스트 했기 때문이다

-> 우리에게 항상 같은 결과를 줄 수 없는 영역이다

 

다시 우리 로또 코드를 보면

 

  • 6개의 숫자를 반환
  • 중복되지 않은 숫자
  • 랜덤하게 반환 -> 테스트 불가능한 것 -> 의도한 전략대로 반환하게끔 풀어서 해야한다. (전략패턴 을 사용한다던지 )

 

예를들면,

public class LottoNumbersGenerator {

      ...

    private final SuhffleStrategy suhffleStrategy;

    public List<Integer> generateTicket() {
        Set<Integer> ticket = new HashSet<>();
        List<Integer> lottoNumbers = collection.createNumbers();

        List<Integer> shuffle = suhffleStrategy.shuffle(lottoNumbers);

        for (int i = 0; ticket.size() < LOTTO_TICKET_LIMIT_NUM; i++) {
            ticket.add(lottoNumbers.get(i));
        }

        return new ArrayList<>(ticket);
    }

  ...

}

 

이런방식으로 전환할 수 있다 .

그러면 의도한 전략대로 테스트값이 나오느냐 테스트할 수 있게 된다.

 

그러므로 우리가 항상 테스트 할 수 있는것, 항상 성공할 수 있는 것, 항상 동일한 결과가 나올 수 있는 것을 테스트해야 한다.

테스트할 수 없는 것을 테스트 하지 말자.

 

3.어떻게 테스트할 것인가?

1. 테스트 할 수 있는 것과 없는것을 구분하자

이 예제를 풀어서 테스트 할 수 있게 해야한다 -> 어떻게?

 

테스트 할 수 없는 메소드를 Bondary 영역까지 끌어 올리면 테스트할 수 있는 영역을 많이 확보할 수 있다.

 

그러면 어떻게 끌어 올리는데? - 예제를 보자

다음 설명은 배달팁 계산 예 이다.

  • 가게들마다 배달팁에 대한 정의들이 있고, 배달팁이랑 배달팁 할인은 시간에 따라 변한다.
  • 특정 시간대에 그시간에 맞는애를 뽑아서 배달팁을 계산하는 로직이다.

 

비즈니스 로직: 현재시간에 해당하는 배달팁의 할인 금액 합산

 

  • 메소드 콜트리를 보자. (호출순서)

 

다음, 테스트 할 수 있는것과 없는것을 보자.

 

  • 가장 하위에 있는 간단한 두 로직이다
public long calculateNow() {
  if (isValid()) {
    return price;
  }
  return 0L;
}

private boolean isValid() {
  LocalDateTime now = LocalDateTime.now();

  return (! now.isBefore(startDateTime) && now.isBefore(endDateTime))
}
  • 간단히 calculateNow를 설명하자면, valid 하면 값을 리턴하고 아니면 0을 리턴하는 로직이다.

 

그러나 isValid가 LocalDateTime.now()를 사용하고 있기 때문에 테스트할 수 없는 영역이다.

  • 시간은 우리는 제어할 수 없는 영역. 그렇기 때문에 isValid() 는테스트 할 수 없는 영역이 된다.

  • 결국에, 맨 아래 테스트할 수 없는 메소드 isValid 때문에 전체가 테스트할 수 없게 만들어지고 물들어간다.

 

 

  • 그러면 이것을 어떻게 Boundary 레이어까지 어떻게 끌어 올려서 테스트 할 수 있게 만들지??

 

 

  • 시간을 isValid 파라미터로 넘길 수있게 된다면 테스트할 수 있게 된다.
    • 하지만, 파라미터를 받기 때문에 메소드 이름도 바뀌어야 한다. (예제코드에서는 바꾸진 않았고요)
public long calculateNow() {
  if (isValid(LocalDateTime.now())) {
    return price;
  }
  return 0L;
}

private boolean isValid(LocalDateTime at) {
  return (! at.isBefore(startDateTime) && at.isBefore(endDateTime))
}

 

즉 비즈니스 로직을 다음과 같이 풀 수 있다.

  • 요구사항 변경을 하는것.

 

현재시간에 해당하는 배달팁의 할인 금액 합산 -> 특정 시간에 해당하는 배달팁과 할인 금액 합산

 

이렇게 해도 아직 calculateNow() 는 테스트하지못한다.

 

결국 테스트 할 수 있게 현재 낮은 레벨을 저 끝 바운더리 영역까지 끌어 올려야 한다.

 

-> 파라미터로 받을 수 있게 바꾸면서 테스트할 수 있는 영역을 점점 끌어올릴 수 있다.

-> 이렇게 테스트 할 수 있는 영역을 많이 늘릴 수 있다.

오.. 다 테스트 가능한 영역이 되었네?

 

  • 근데 도대체 어디까지 올려야 하는 걸까??
    • 이부분은 매우 어려운 영역이다..

 

테스트 불가능한 영역을 Boundary Layer로 올려서 테스트 가능하도록 변경한다

  • 바운더리 레이어를 한 모듈로서(응집 덩어리로 가치 있는곳)으로 정하고, 가장 바깥쪽이 적당하다.

 

2. 어떻게 테스트할 것 인가?

저 로또 프로그램은 콘솔 프로그램이였다.

  • 요구사항을 바꿔서 로또를 웹으로 구현해봐라!

 

  • 스프링을 사용하면 코드를 @Bean으로 만들어야겠지?
    • -> @Bean, @Component, @Service 같은 어노테이션을 사용해서?

 

Test는 Boot로 하자..?

 

@SpringBootTest를 사용해서 테스트하면 무척 편하게 테스트가 가능하다.

하지만 스프링을 사용하고 있다고 꼭 스프링 컨텍스트를 사용해야할지 고민이 필요하다.

 

  • SpringContext를 사용하면 테스트가 느리다 -> 빠른 피드백을 받을 수 없다.

 

근데 우리는 자바 개발자인가 스프링 개발자인가?

 

자바 테스트인지 테스트 스프링 프레임워크 위에서만 돌아갈 수 있는 개발자인지 생각이 필요하다.

  • 저 컴포넌트들이 Bean이라고 해서 Spring Context가 꼭 필요한건 아니다.

결론

Context, Framework에 종속적이지 않은 테스트를 우선시 하자

 

3. Test Double

테스트할 수 없는 영역에 대한 외부 요인을 부여할 수 있도록 도와주는 도구.

자바에서는 "mockito"가 이런 도구에 해당

-> Mocking!

 

example

가짜 객체를 만들어서 저 안에서 일어나는 일을 테스트 안에서 제어하는 일을 할 수 있게 된다.

 

그렇다면 무엇을 Test Double로 처리할것인가?

  1. 위에 스택 최상단이 콜스택 최상단
  2. 1번 2번 3번 4번 코드를 수행하는 장표

 

  • 우리는 4번째 코드를 테스트 하고싶다.

 

  • 어디를 TestDouble로 해야 할까?

우린 초록색을 테스트 하고싶은데 하위 구현체인 1,2,3,4가 뭘 받고 뭘 리턴해야하는지 알아야 한다는 문제가 있다..

 

4번만 테스트할 때 1,2,3은 기대한 값으로 반환하게한다는 것은 결국 1,2,3이 어떻게 구현되어있는지 구현을 알아야한다.

 

  • 2번쪽의 반환 타입을 변경하거나
  • 3번의 입력 값을 변경하면 테스트가 계속 깨진다. -> 인자를 여러개..

이럴때 TestDouble을 사용하면 편하긴 하다.

 

그렇다고 TestDouble을 남용하지 말자.

  • TestDouble(mock)을 남용을 하면 구현 테스트로 유도할 수도 있다.
    • (다시 생각해보자. 우린 구현 내용보다, 설계에 대한 테스트를 해야 한다. )

그렇다면 언제쯤 어떻게 남용하지 않고. Testdouble을 사용해야하나?

간단한 방식은, 바운더리 컨텍스트까지 올렸을 때 더 이상 테스트 할 수 없는 영역이 있을 때 ,

그것에 대한 통합 테스트를 이끌어 내야 할 때 사용하는경우에 사용한다고 한다.

 

그렇지 않은 경우에는 2,3번도 하나의 의미를 갖는 모듈일 때 충분히 테스트되고 검증되서 올라온 애들이라면 돌아가도 된다.

충분히 테스트되고 검증된 모듈은 괜찮다고 할 수 있다.

  • (할 수 있다가 OK가 아니다. 긴장하자 소프트웨어는 항상 버그가 있다.)

 

순수 자바 어플리케이션으로 테스트할 수 없는것에는 뭐가 있을까?

  • 저장소에 대한 입출력 검증 (쿼리가 잘 나갔는지에 대한)
  • SPEC 검증하고싶을때
    • 내부 Controller -> 컨트롤러에 대한 사용법 미숙 등
    • 외부 API

 

4. Embedded 시스템

  • 이런 임베디드 시스템을 사용하면 제어할 수 없는 영역을 제어 가능하도록 만들 수 있다. (너무 남용하면 안좋음).

 

왜 권장하냐? -> SpringFramework를 사용하면 추상화된 덩어리들을 사용하기 때문.

우리가 구현하진 않았지만 편리하게 돌아가는 영역이 많다!

-> 라이브러리에 대한 테스트를 하고싶을 때 임베디드 시스템에 대한 테스트를 하자 .

  • 다양한 Enbedded Test용 시스템이 있다.

그럼 언제부터 언제까지 임베디드 시스템이 살아있어야 하는데?

애플리케이션이 시작되고 종료될 때까지?


No!


 

임베디드 시스템은 테스트 사이클 내에서만 존재해야 한다.

  • 테스트와 라이프사이클을 동일하게 하자

그럼 로컬에서 띄워서 테스트 하면 되는거 아냐?

  • 테스트 정확도는 로컬이 더 빠르다. 그 러 나
  • 임베디드 시스템은 라이브러리 버전이라든지 대응속도가 느려서 따라가는게 느리다.

 

그것만으로 납득이 안되는데? 왜 로컬보다 임베디드 시스템이냐?

피드백 속도와 안정성이 왜 임베디드가 더 높냐?

  • 테스트는 상호 독립적으로 돌아가야 한다.
    • 저 안에 들어있는 내용을 매번 넣고 비워주는 작업을 해줘야 한다.

누구라도 쉽게 실행하고 테스트할 수 있어야 하고, 받자마자 바로 실행할 수 있어야 하기 때문이다.

  • 이것을 임베디드 시스템이 도와준다.

5. End Point 테스트

Spring FrameWork에서 제공해주는 End Point Test

  1. MockMvc
  2. Rest Assured
  3. WebTestClient

 

EndPoint 테스트 팁

우리가 요청 스펙 검증이랑 응답 스펙을 검증하고 싶은데,

이게 생각해보면 우리가 그 안에 있는 비지니스를 전부 알아야 한다

테스트의 목적은 요청과 응답 스팩 검증만으로 제한하는게 정신 건강에 좋을수도 있다.

즉 상호 독립적으로, 컨트롤러만 테스트 할 수 있게 하고, 나머지 계층은 Mock하는것이다.

  • 모든 케이스를 다 고려하는건 너무 힘들다.

또한, Spring REST Docs를 사용하면 테스트를하면서 문서를 만들 수 있다.

  • 테스트와 일맥상통하는 문서까지 뽑을 수 있다는 장점이 있다.

4.TIP & RULE

1 테스트 상호 독립

테스트는 상호 독립적으로 작성을 해야한다. 테스트 1번에서 삽입하고, 테스트 2번에서 수정하는식으로 작동하면 안된다.

예제로

1번에서 삽입하고 2번에서 수정을한다.

이러면 순서가 항상 일치해야 한다.

모든 테스트의 순서와 관계를 생각하며 테스트를 작성하기 어렵다.

그러므로 우리가 테스트를 작성할 땐 항상 독립적으로 작성해야 한다.

임베디드 시스템을 사용해도 같다. 테스트마다 독립적으로 실행되어야 한다.

공유되는 자원은 테스트가 끝나고 지워주는 룰을 두고 하는것이 좋다.

테스트 순서가 필요하면 JUnit5의 DynamicTest를 통해서 하나의 라이프 사이클에서 돌아가도록 수행한다.

테스트 안에 의도와 목적이 드러나도록 작성한다.

누군가가 테스트 코드만 봐도 비즈니스의 플로우를 파악할 수 있어야한다.

  • 테스트 코드 역시 가독성이 중요하다.

3. 테스트 코드도 리팩토링 대상이다.

시간이 흐르면서 코드와 테스트코드도 들어난다.

코드의 양이 늘어가면서 가독, 안정성, 요구사항 정리 등 비즈니스 코드와 동일한 수준의 리팩토링이 함께 이루어져야한다.

정리

무엇을 테스트 할 것인가?

  1. 설계를 테스트
  2. 테스트 가능한것을 테스트

어떻게 테스트 할 것인가?

  1. 테스트 불가능한 영역을 Boundary Layer까지 올려서 테스트 가능하도록 변경
  2. Context, Framework 종속적이지 않도록 테스트
  3. Test Double 사용
  4. Embedded System사용
  5. EndPoint Test 도구 사용하여 내부 API Spec 테스트
  6. Spring cloud Contract로외부 API SPec 테스트

TIP & RULE

  1. 테스트는 상호 독립적으로 작성
  2. 테스트 안에서 의도와 목적이 모두 드러나도록 작성
  3. 테스트 코드도 리팩토링

가장 중요한것은 안정감과 자신감이다

기준을 최대한 지키되 기준을 지키기 위해 안정감과 자신감을 포기할 필요는 없다.