Unit Testing

목(mock)사용하는 테스트의 취약성

25G 2024. 2. 18. 11:19

목과 스텁

흔히 테스트를 할때 다른 시스템 모듈의 의존성을 최소화 하기 위해서 목과 스텁을 사용하는데 목은 테스트 대상 시스템과 그 협력자 사이의 상호 작용을 검사할 수 있는 테스트 대역이다.

테스트 대역은 모든 유형의 비운영용 가짜 의존성을 설명하는 포괄하는 용어 여기서 목(목,스파이)과 스텁(스텁,더미,페이크)로 나뉜다.

두 유형의 차이점

  • 목은 외부로 나가는 상호 작용을 모방하고 검사하는 데 도움이 된다. 이러한 상호작용은 테스트 대상 시스탬이 상태를 변경하기 위한 의존성을 호출하는 것에 해당 (이메일 전송을 하는 부분 등등)
  • 스텁은 내부로 들어오는 상호작용을 모방하는 데 도움이 된다. 이러한 상호작용은 테스트 대상 시스템이 입력 데이터를 얻기 위한 의존성을 호출하는 것에 해당한다.(DB에서 데이터를 들고오는 부분 등등)

스텁으로 상호 작용을 검증하지 말것

위 두가지 차이가 나는 이유는 스텁은 상호 작용을 검증하지 말라는 지침에서 비롯된다. 테스트 대상 시스템에서 스텁으로의 호출은 테스트 대상 시스템이 생성하는 최종 결과가 아니다 .이러한 호출은 최종 결과를 산출하기 위한 수단일 뿐. 즉 스텁과의 상호 작용을 검증하는 것은 취약한 테스트를 야기하는 일반적인 안티패턴이다. 왜냐하면 거짓양성이 생기기 때문. 최종결과가 아닌 사항만 검증하는 이러한 관행을 과잉명세라고 한다

목과 스텁은 명령과 조회에 어떻게 관련돼 있나?

목과 스텁의 개념은 명령조회 분리CQS원칙과 관련이 있다. 모든 메서드는 명령이거나 조회여야 하며 이 둘은 혼용해서는 안된다. 이 원칙을 따르려고 하면 목은 명령 스텁은 조회에 가까우며 메서드가 사이드 이펙트가 있는지 (void인지아닌지) 확인하여 이 둘을 분리하면 코드를 읽기 쉬워 진다.

식별할 수 있는 동작은 공개 api와 다르다.

모든 제품코드는 2차원으로 분류된다.

  • 공개 api 또는 비공개 api
  • 식별할 수 있는 동작 또는 구현 세부 사항
    이때 식별할 수 있는 동작과 내부 구현 세부 사항에는 미묘한 차이가 있다. 코드가 시스템의 식별할 수 있는 동작이려면 목표를 달성하는데 도움이 되는 연산을 노출하거나 목표를 달성하는데 도움이 되는 상태를 노출해야한다. 하지만 구현 세부 사항은 이 두 가지 중 아무것도 하지 않는다. 코드가 식별할 수 있는 동작인지 여부는 해당 클라이언트가 누구인지, 클라이언트의 목표가 무엇인지에 달려있다.
    잘 설계된 api 에서 식별할 수 있는 동작은 공개 api와 일치하는 반면 모든 구현 세부 사항은 비공개api뒤에 숨어있다.
    대부분의 경우에 단일한 목표를 달성하고자 클래스에서 호출해야 하는 연산의 수가 1보다 크면 해당 클래스에서 구현 세부 사항을 유출할 가능성이 있다.

구현 세부사항을 공개하지 말라

좋은 단위 테스트와 잘 설계된 api 사이에는 본질적인 관계가 있다 모든 구현 세부 사항을 비공개로 하면 테스트가 실별할 수 잇는 동작을 검증하는 것 외에는 다른 선택지가 없어지고 자연스럽게 리팩터링 내성도 좋아진다. 즉 단위테스트도 자동으로 좋아지게 된다.

목과 테스트 취약성 간의 관계

육각형 아키텍처 정의

  • 도메인 계층과 애플리케이션 서비스 계층 간의 관심사 분리
    • 도메인 계층은 해당 비즈니스 로직에 대해서만 책임을 저야하고 다른 모든 책임에서는 제외돼야 한다. 즉 DB에 검색하는 거나 이메일 전송하는 등의 기능은 서비스 계층에서 해결해야한다. 반대로 서비스에는 어떠한 비즈니스 로직도 있어서는 안된다.
  • 애플리케이션 내부 통신
    • 육각형 아키텍처는 애플리케이션 서비스 계층에서 도메인 계층으로 흐르는 단반향 의존성 흐름을 규정한다. 도메인 계층 내부 클래스는 도메인 계층 내부 클래스 끼리 서로 의존하고 서비스 계층의 클래스에 의존하지 않는다.
  • 애플리케이션 간의 통신
    • 외부 애플리케이션은 서비스 계층에 있는 공통 인터페이스를 통해 해당 애플리케이션에 연결된다. 아무도 도메인 계층에 직접 접근할 수 없다.

시스템 내부 통신과 시스탬 간에 통신

일반적인 애플리케이션에는 시스템 내부통신과 시스템간에 통신이 있다.(시스템 내부 통신은 세부구현사항이고 시스템 간 통신은 그렇지 않다.) 이는 시스템의 성장과도 관련이 있는데 바로 하위호환성을 지키는 것이다. 시스탬 내부에서 하는 리팩터링과 다르게 시스템 외부 애플리케이션과 통신할때는 항상 외부 시스템이 이해할 수 있도록 유지해주어야한다. 이때 목을 사용하면 시스템과 외부 애플리케이션 간의 통신패턴을 확인할 때 좋다. 반대로 시스템 내 클래스 간의 통신을 검증하는데 사용하면 테스트가 구현 세부사항과 결합되면서 리팩터링 내성 지표가 미흡해진다.

그렇다고 모든 프로세스를 외부 의존성을 목으로 해야하는 것은 아니다.

의존성 유형의 세가지 종류

  • 공유 의존성: 테스트 간에 공유하는 의존성 (제품코드가 아님)
  • 프로세스 외부 의존성: 프로그램의 실행 프로세스 외에 다른 프로세스를 점유하는 의존성
  • 비공개 의존성: 공유하지 않는 모든 의존성

애플리케이션과 외부 시스템 간의 통신 패턴을 항상 지켜야 하는 요구사항은 하위 호환성을 지켜야 한다는 점에서 비롯되고 외부 시스템과 통신하는 방식을 지켜야 한다. 그러나 애플리케이션이 외부 시스템에 대한 프록시 같은 역할을 하고 클라이언트가 직접 접근할 수 없으면 하위 호환성 요구사항은 사라진다. 이러한 외부 시스탬과 애플리케이션을 같이 배포할 수 있으면 클라이언트에 영향을 미치지 않는다. 이렇게되면 이는 구현 세부사항으로 넘어가게 되는것.
대표적인 좋은 예로 데이터베이스가 있다.

목을 사용한 동작검증

종종 목이 동작을 검증한다고 한다. 하지만 대부분의 경우 그렇지 않고 클라이언트 목푤를 거슬러 올라갈 수 잇는 동작에 해당한다. 목은 애플리케이션의 경계를 넘나드는 상호 작용을 검증할 때와 이러한 상호작용의 사이드 이펙트가 외부 환경에서 보일때만 동작과 관련이 있다.

출처: unit Testing : 블라디미르코리코프 지음