7장. 아키텍처 요소 테스트하기

Seungjae·2022년 6월 21일
0

우아한 스터디

목록 보기
8/10
post-thumbnail

테스트 피라미드


업로드중..

  • 만드는 비용이 적고, 유지보수하기 쉽고, 빨리 실행되고, 안정적인 작은 크기의 테스트들에 대해 높은 커버리지를 유지해야한다.

  • 테스트가 비싸질수록 테스트의 커버리지 목표는 낮게 잡아야한다. 그렇지 않으면 새로운 기능을 만드는 것보다 테스트를 만드는데 시간을 더 쓰게 된다.

  • 위의 ‘시스템 테스트’, ‘통합 테스트’, ‘단위 테스트’의 정의는 맥락에 따라 다를 수 있다.

  • 단위 테스트 → 일반적으로 하나의 클래스를 인스턴스화하고 해당 클래스의 인터페이스를 통해 기능들을 테스트

    • 테스트 대상 클래스가 다른 클래스에 의존하고 있다면 의존되는 클래스들은 인스턴스화하지 않고 테스트 동안 필요한 작업들을 흉내내는 Mock으로 대체!
  • 통합 테스트 → 연결된 여러 유닛을 인스턴스화하고 시작점이 되는 클래스의 인터페이스로 데이터를 보낸 후, 유닛들의 네트워크가 기대한대로 잘 동작하는지 검증

  • 시스템 테스트 → 애플리케이션을 구성하는 모든 객체 네트워크를 가동시켜 특정 유스케이스가 전 계층에서 잘 동작하는지 검증

단위 테스트로 도메인 엔티티 테스트하기


  • 책 내의 Account에 대한 단위 테스트

    1. 특정 상태의 Account를 인스턴스화
    2. withdraw() 메서드 호출
    3. 출금 성공했는지 검증
    4. Account 객체의 상태에 대해 기대되는 부수효과들 잘 일어났는지 확인
  • 만들고 이해하기 쉬운 편이고, 아주 빠르게 실행된다.

  • 이런 식의 단위 테스트는 도메인 엔티티에 녹아 있는 비즈니스 규칙을 검증하기에 가장 적절한 방법이다.

단위 테스트로 유스케이스 테스트하기


  • 테스트 가독성을 높이기 위해 행동-주도 개발에서 일반적으로 사용되는 방식으로 작성

    • given / when / then
      • given → 테스트에 필요한 것들을 준비
      • when → 테스트 하려하는 행위 호출
      • then → 테스트로 인해 의도된 행위가 호출되었는지, 원하는 상태 변화가 발생했는지 검증
  • 이때 테스트에서 특정 메서드와 상호작용했는지 여부를 검증하는 경우

    • 테스트가 코드의 행동 변경뿐만 아니라 코드의 구조 변경에도 취약해진다.
    • 코드가 리팩토링되면 테스트도 변경될 확률이 높아진다.
    • 즉, 신중하게 생각해야한다.
    • 모든 동작을 검증하려는 것보다, 중요한 핵심에 집중해서 테스트하자!
  • 책 내의 예시 테스트는 단위 테스트지만 의존성의 상호작용을 테스트하고 있기 때문에 통합 테스트와 유사

    • 하지만 Mock으로 작업하고 있고, 실제 의존성을 관리해야 하는 것은 아니기 때문에 완전한 통합 테스트에 비해 만들고 유지보수하기 쉽다!

통합 테스트로 웹 어댑터 테스트하기


  • 웹 어댑터 테스트는 아래의 것들이 모두 기대한 대로 동작하는지 검증

    1. JSON 문자열 등의 형태로 HTTP를 통해 입력 받기
    2. 입력에 대한 유효성 검증
    3. 유스케이스에서 사용할 수 있는 포맷으로 매핑
    4. 유스케이스에 전달
    5. 유스케이스의 결과를 JSON으로 매핑
    6. HTTP 응답을 통해 클라이언트에 반환
  • 웹 어댑터 테스트

    • Mock HTTP 요청을 웹 컨트롤러에 보낸다.
    • 요청 바디는 JSON 문자열의 형태로 입력 객체에 포함한다.
    • HTTP의 응답 상태가 기대했던 것과 같은지 검증하고, 모킹한 유스케이스가 잘 호출됐는지 검증한다.
  • MockMvc를 사용해 모킹했기 때문에 실제로 HTTP 프로토콜을 통해 테스트한 것은 아니다.

    • 프레임워크가 HTTP 프로토콜에 맞게 모든 것을 적절히 잘 변환한다고 믿는 것!
    • 프레임워크를 테스트할 필요는 없기 때문!

테스트 해야할 것, 테스트 할 필요없는 것

  • 위에 나온대로 프레임워크를 테스트할 필요는 없다.
    • Why?
    • 프레임워크는 이미 똑똑하신 분들이 많은 검증을 거쳐서 만들어 놓으신 것이다. 바퀴를 다시 만들 필요는 없다는 말과 같은 의미이다.
  • 이러한 맥락을 바탕으로 테스트가 필요한 것, 필요 없는 것을 구분하는 것도 매우 중요한 포인트라고 생각한다.
  • 이 테스트는 왜 단위 테스트가 아닌 통합 테스트일까?
    • 컨트롤러 클래스만 테스트한 것 같지만, 그렇지 않다. 훨씬 더 많은 일이 벌어지고 있다.
    • @WebMvcTest 애너테이션은 스프링이 특정 요청 경로, 자바와 JSON 간의 매핑, HTTP 입력 검증 등에 필요한 전체 객체 네트워크를 인스턴스화하도록 만든다.
    • 테스트에서는 웹 컨트롤러가 이 네트워크의 일부로서 잘 작동하는지 검증
    • 즉, 웹 컨트롤러가 스프링 프레임워크에 강하게 묶여 있다!
      • 격리된 상태로 테스트하기보다는 이 프레임워크와 통합된 상태로 테스트하는 것이 합리적
    • 웹 컨트롤러만 따로 단위테스트로 테스트한다면 모든 매핑, 유효성 검증, HTTP 항목에 대한 커버리지가 낮아지고, 프레임워크를 구성하는 이런 요소들이 프로덕션 환경에서 정상적으로 작동하는지 확신할 수 없게 된다.

통합 테스트로 영속성 어댑터 테스트하기


  • 웹 어댑터 테스트와 비슷한 이유로, 영속성 어댑터도 통합 테스트 적용이 합리적이다.

  • @DataJpaTest 사용

    • 스프링 데이터 리포지토리들을 포함해서 DB 접근에 필요한 객체 네트워크를 인스턴스화해야 한다고 스프링에 알려준다.
  • 이 테스트에서 DB를 모킹하지 않는다.

    • 즉, DB에 접근하게 된다.
  • 모킹을 이용할 경우, 실제 DB와 연동했을 때, SQL 구문의 오류나 DB 테이블과 자바 객체 간의 매핑 에러 등으로 문제가 생길 확률이 높아지기 때문!

  • 스프링에서는 기본적으로 인메모리 DB를 테스트에서 사용한다. 하지만 프로덕션 환경에서 인메모리 DB를 사용하는 경우는 적기 때문에, 인메모리 DB에서 테스트를 통과했더라도 실제 DB에서 문제가 생길수도 있다. (DB 방언 같은 것 때문에..!)

    • 즉, 영속성 어댑터 테스트는 실제 DB를 대상으로 진행하는 것이 좋다!
    • 인메모리DB를 사용하지 않고 실제 DB를 사용하고자 하는 경우 @AutoConfigureTestDatabase를 이용하여 손쉽게 설정할 수 있다.

시스템 테스트로 주요 경로 테스트하기


  • 시스템 테스트는 전체 애플리케이션을 띄우고 API를 통해 요청을 보내고, 모든 계층이 조화롭게 잘 동작하는지 검증한다.

  • @SpringBootTest 사용

    • 스프링이 애플리케이션을 구성하는 모든 객체 네트워크를 띄우게 한다.
    • 책 내 예제에서는 랜덤 포트로 애플리케이션을 띄우도록 하고 있다.
  • MockMvc가 아닌 TestRestTemplate를 이용

    • 실제 HTTP 통신을 수행(프로덕션 환경에 조금 더 가깝다.)
  • 실제 HTTP 통신을 하는 것처럼 실제 출력 어댑터도 사용

  • 지저분한 로직들은 헬퍼 메서드 안으로 숨겼다. → 가독성 증가

    • 이 헬퍼 메서드들은 여러 가지 상태를 검증할 때 사용할 수 있는 DSL(도메인 특화 언어, 특정한 도메인을 적용하는 데 특화된 언어)을 형성
  • 시스템 테스트는 훨씬 더 실제 사용자와 유사하다. → 사용자 관점에서 애플리케이션을 검증할 수 있다.

    • 어휘를 사용하면 도메인 전문가가 테스트에 대해 이해하고, 생각하고, 피드백을 줄 수 있다.
    • JGiven 같은 라이브러리를 활용
  • 시스템 테스트는 단위 테스트와 통합 테스트가 발견하는 버그와는 또 다른 종류의 버그를 발견해서 수정할 수 있게 해준다.

  • 시스템 테스트는 여러 개의 유스케이스를 결합해서 시나리오를 만들 때 더 빛이 난다.

    • 시나리오 → 사용자가 애플리케이션을 사용하면서 거쳐갈 특정 경로
    • 중요한 시나리오들이 커버된다면 최신 변경사항들이 애플리케이션을 망가뜨리지 않았음을 가정할 수 있고, 배포될 준비가 됐다는 확신을 가질 수 있다.

얼마만큼의 테스트가 충분할까?


  • 라인 커버리지는 테스트 성공을 측정하는 데 있어서 잘못된 지표이다.

잘못된 지표..?

  • 잘못된 지표보다는 절대적이지 않은 지표라고 보는게 맞다고 생각한다.
  • 라인 커버리지가 높다고 검증이 잘 된, 잘 만들어진 프로젝트라고 할 수는 없다.
  • 하지만 라인커버리지가 낮으면 보통 검증이 제대로 되지 않은, 테스트가 여러 상황을 커버하지 못하는 프로젝트이다.
  • 물론 상황마다 다르겠지만, 외부와 분리된 도메인 코드들이 들어있는 패키지의 커버리지가 매우 낮게 나온다면 그것은 주의깊게 다시 확인할 필요가 있다고 생각한다.
  • 얼마나 마음 편하게 SW를 배포할 수 있느냐를 테스트의 성공 기준으로 잡는 것이 좋다!

    • 더 자주 배포할수록, 테스트를 더 신뢰할 수 있다!
  • 각각의 프로덕션 버그에 대해서 테스트가 그 버그를 잡지 못한 이유에 대해 생각하고, 기록하고 커버할 수 있는 테스트를 추가하는 것이 중요!

  • 책에서 소개하는 육각형 아키텍처에서 사용하는 테스트 전략

    1. 도메인 엔티티를 구현할 때는 단위 테스트로 커버하자
    2. 유스케이스를 구현할 때는 단위 테스트로 커버하자
    3. 어댑터를 구현할 때는 통합 테스트로 커버하자
    4. 사용자가 취할 수 있는 중요 애플리케이션 여로(시나리오)는 시스템 테스트로 커버하자
  • 테스트 작성 시 주의 사항

    • 개발 후가 아닌 개발 중에 테스트가 이뤄져야한다!
      • 개발 후에 하는 테스트는 그저 귀찮게 느껴진다...
    • 리팩토링할 때마다 테스트 코드도 변경해야 한다면 테스트는 테스트로서의 가치를 잃는다.
      • 테스트는 테스트하려는 대상의 구현 세부사항을 드러내면 안된다!

유지보수 가능한 SW를 만드는 데 어떻게 도움이 될까?


  • 육각형 아키텍처는 도메인 로직과 바깥으로 향한 어댑터를 깔끔하게 분리

    • “핵심 도메인 → 단위 테스트 / 어댑터 → 통합 테스트” 로 처리하는 명확한 테스트 전략 정의 가능!
  • 입출력 포트는 테스트에서 아주 뚜렷한 모킹 지점

    • 포트가 아주 작고 핵심만 담고 있다면 모킹하는 것이 아주 쉬울 것이다.
  • 모킹하는 것이 너무 버겁거나, 코드의 특정 부분을 커버하기 위해 어떤 종류의 테스트를 써야할지 모르겠다.

    • ⇒ 이것은 경고 신호다!
    • 테스트는 아키텍처의 문제에 대해 경고
    • 테스트가 피드백을 주고 있는 것이다!
profile
코드 품질의 중요성을 아는 개발자 👋🏻

0개의 댓글

관련 채용 정보