BottleH Blog

Unit Testing - 5장 목과 테스트의 취약성

    Tags

  • Test
Unit Testing - 5장 목과 테스트의 취약성 thumbnail

목과 테스트 취약성 사이에는 깊고 불가피한 관련이 있다.

📖 5.1 목과 스텁 구분


🔖 5.1.1 테스트 대역 유형

테스트 대역

  • 모든 유형의 비운영용 가짜 의존성을 설명하는 포괄적인 용어
  • 스턴트 대역에서 유래됨
  • 테스트를 편리하게 하는 것
  • Mock
    • 외부로 나가는 상호 작용(SUT가 상태를 변경하기 위한 의존성 호출)을 모방하고 검사하는 데 도움이 됨.
    • 스파이는 수동으로 작성하는 직접 작성한 목이다.
    • 은 목 프레임워크의 도움을 받아 생성된다.
  • Stub
    • 내부로 들어오는 상호 작용(SUT가 입력 데이터를 얻기 위한 의존성을 호출)을 모방하는 데 도움이 됨.
    • 더미는 단순하고 하드코딩된 값이고, 최종 결과를 만드는 데 영향을 주지 않는다.
    • 스텁은 시나리오마다 다른 값을 반환하게끔 구성할 수 있도록 필요한 것을 완전히 다 갖춘 의존성이다.
    • 페이크는 아직 존재하지 않는 의존성을 대체하고자 구현한다.

목은 SUT와 관련 의존성 간의 상호 작용을 모방하고 검사하는 반면, 스텁은 모방만 한다.

🔖 5.1.2 도구로서의 목과 테스트 대역으로서의 목

도구로서의 목을 상요해 목과 스텁, 두 가지의 테스트 대역을 생성할 수 있기 때문에 도구로서의 목과 테스트 대역으로서의 목을 혼동하지 않는 것이 중요하다.

🔖 5.1.3 스텁으로 상호 작용을 검증하지 말라

스텁과의 상호 작용을 검증하는 것은 취약한 테스트를 야기하는 일반적인 안티 패턴이다.

  • SUT에서 스텁으로의 호출은 SUT가 생성하는 최종 결과가 아니다.
  • 최종결과가 아닌 사항을 검증하는 이러한 관행을 과잉 명세라고 부른다.

🔖 5.1.4 목과 스텁 함께 쓰기

테스트 대역이 목이면서 스텁인 경우, 목이라고 부르지 스텁이라고 부르지는 않는다. 이름을 하나 골라야 하기도 하고, 목이라는 사실이 스텁이라는 사실보다 더 중요하기 때문이다.

🔖 5.1.5 목과 스텁은 명령과 조회에 어떻게 관련돼 있는가?

목과 스텁의 개념은 CQS(명령 조회 분리) 원칙과 관련되어 있다.

  • CQS와 CQRS
    • 명령
    • 부작용(객체 상태 변경 등)을 일으키고, 어떤 값도 반환하지 않는 메서드
  • 스텁
    • 조회
    • 부작용이 없고, 값을 반환

항상 CQS 원칙을 따를 수 있는 것은 아니지만 가능할 때마다 따르는 것이 좋다.

📖 5.2 식별할 수 있는 동작과 구현 세부 사항


🔖 5.2.1 식별할 수 있는 동작은 공개 API와 다르다

모든 제품코드는 다음과 같이 분류할 수 있다.

  • 공개 API ↔️ 비공개 API
    • Java의 경우 접근제한자로 구분 가능
  • 식별할 수 있는 동작 ↔️ 구현 세부사항
    • 식별할 수 있는 동작의 경우 클라이언트가 목표를 달성하는 데 도움이 되는 연산과 상태 노출
    • 구현 세부사항은 위의 두 가지 중 아무것도 하지 않는다.

이상적으로 시스템의 공개 API는 식별할 수 있는 동작과 일치해야 하며, 모든 구현 세부사항은 클라이언트 눈에 보이지 않아야 한다.

🔖 5.2.2 구현 세부 사항 유출: 연산의 예

public class User { public String Name; public String normalizeName(String name) { String result = (StringUtils.hasText(name) ? name : "").trim(); if (result.length() > 50) { return result.substring(0, 50); } return result; } } public class UserController { public void renameUser(String userId, String newName) { User user = new User(userId); String normalizeName = user.normalizeName(newName); user.setName(normalizeName); } }

위 코드의 문제점은 클라이언트의 목표에 도움이 되지 않는 작업이 노출되는 것에 있다. 즉, 불변 속성을 만족시켜야 한다.

🔖 5.2.3 잘 설계된 API와 캡슐화

장기적으로 코드베이스 유지 보수에서는 캡슐화가 중요하다.

  • 복잡도 때문
  • 캡슐화는 궁극적으로 단위 테스트와 동일한 목표를 달성한다.

구현 세부 사항을 숨기면 클라이언트의 시야에서 클래스 내부를 가릴 수 있기 때문에 내부를 손상시킬 위험이 적다.

데이터와 연산을 결합하면 해당 연산이 클래스의 불변성을 위반하지 않도록 할 수 있다.

🔖 5.2.4 구현 세부 사항 유출: 상태의 예

식별할 수 있는 동작 구현 세부 사항
공개 좋음 나쁨
비공개 해당 없음 좋음

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


🔖 5.3.1 육각형 아키텍처 정의

애플리케이션 서비스 계층은 도메인 계층 위에 있으며 외부 환경과의 통신을 조정한다.

  • 예를 들어 RESTful API 기반의 애플리케이션은 모든 API 요청이 애플리케이션 서비스 계층에 먼저 도달하게 된다.
  • 이 계층은 도메인 클래스와 프로세스 외부 의존성 간의 작업을 조정한다.

애플리케이션 서비스에 대한 조정의 예시

  • 데이터베이스를 조회하고 해당 데이터로 도메인 클래스 인스턴스 구체화
  • 해당 인스턴스에 연산 호출
  • 결과를 데이터베이스에 다시 저장

애플리케이션 서비스 계층과 도메인 계층의 조합은 육각형을 형성하며, 이 육각형은 애플리케이션 자체를 나타낸다. 애플리케이션은 또 다른 애플리케이션과 소통할 수 있으며 다른 애플리케이션도 역시 육각형으로 나타낼 수 있다.

이 육각형 아키텍쳐라는 용어는 앨리스터 코오번(Alistair Cockburn) 이 처음 소개했으며, 아래 세 가지 중요한 지침을 강조한다.

  • 도메인 계층과 애플리케이션 서비스 계층 간의 관심사의 분리
    • 비즈니스 로직은 애플리케이션의 가장 중요한 부분으로, 도메인 계층은 비즈니스 로직에 대해서만 책임을 져야한다. 즉, 외부 애플리케이션과의 통신이나 데이터 검색등은 애플리케이션 서비스에만 귀속되어야 하는 것이다.
  • 애플리케이션 내부 통신
    • 육각형 아키텍쳐는 애플리케이션 서비스 계층에서 도메인 계층으로 흐르는 단방향 의존성 흐름을 규정한다.도메인 계층 내부 클래스는 도메인 계층 내부 클래스끼리 서로 의존하고, 애플리케이션 서비스 계층의 클래스는 의존하지 않는다.
  • 애플리케이션 간의 통신
    • 외부 애플리케이션은 애플리케이션 서비스 계층에 있는 공통 인터페이스를 통해 해당 애플리케이션에 연결되며, 직접적으로 도메인 계층에 연결되지 않는다.

🔖 5.3.2 시스템 내부 통신과 시스템 간 통신

일반적인 애플리케이션에는 시스템 내부 통신과 시스템 간 통신이 있다.

  • 연산을 수행하기 위한 도메인 클래스 간의 협력은 식별가능한 동작이 아니므로, 시스템 내부 통신은 구현 세부 사항에 해당한다.
  • 시스템 외부 환경과 통신하는 방식은 전체적으로 해당 시스템의 식별가능한 동작을 나타낸다.

Mock을 사용하면 시스템과 외부 애플리케이션 간의 통신 패턴을 확인 할 때 좋다.

📖 5.4 단위 테스트의 고전파와 런던파 재고

런던파는 불변 의존성을 제외한 모든 의존성에 Mock 사용을 권장하며, 시스템 내 통신과 시스템 간 통신을 구분하지 않는다.

  • 이렇게 무분별하게 Mock을 사용하게 되는 경우, 종종 구현 세부 사항과 결합하여 리팩토링 내성을 낮추는 결과를 나타내기도 한다.

고전파는 테스트 간의 공유하는 의존성만 교체하는 것을 권장하므로 이 문제에 있어 좀 더 유리한 입장이다.

  • 고전파 역시 Mock의 사용을 권장하므로 시스템 간 통신에 대해서는 이상적이지 않다고 볼 수 있다.

🔖 5.4.1 모든 프로세스 외부 의존성을 목으로 해야 하는 것은 아니다

프로세스 외부 의존성과 Mock을 설명하기 전에 의존성 유형에 대해서 다시 살펴보자.

공유 의존성이 프로세스 외부에 있는 것이 아니면 각 테스트 실행시 해당 의존성을 새 인스턴스로 써서 재사용을 피하기 쉽다. 따라서, 공유 의존성이 프로세스 외부에 있으면 테스트가 더욱 복잡해진다. 이를 위해 공유 의존성을 테스트 대역인 Mock과 Stub으로 교체하는 것이 일반적인 접근 방법이다. 그러나 모든 프로세스 외부 의존성을 Mock으로 치환할 필요는 없다.

프로세스 외부 의존성이 애플리케이션을 통해서만 접근할 수 있다면 이러한 통신 방식은 시스템에서 식별할 수 있는 동작이 아닐 것이다. 결론적으로 외부에서 관찰할 수 없는 프로세스 외부 의존성은 애플리케이션 일부로 작용한다.

반면 애플리케이션이 외부 시스템에 대한 프록시 같은 역할을 하고 클라이언트가 직접 접근할 수 없으면 하위 호환성 요구 사항은 사라진다.

🔖 5.4.2 목을 사용한 동작 검증

종종 목이 동작을 검증한다고 하지만 대부분의 경우 그렇지 않다. 목표를 달성하고자 각 개별 클래스가 이웃 클래스와 소통하는 방식은 식별할 수 있는 동작과는 아무런 관계가 없다.

목은 애플리케이션의 경계를 넘나드는 상호 작용을 검증할 때와 이러한 상호 작용의 부작용이 외부 환경에서 보일 때만 동작과 관련이 있다.

Written by@BottleH
Back-End Developer

GitHub