Book Notes

[개발서적] Unit Testing(단위 테스트) 핵심 요약 및 정리

feel2 2024. 3. 25. 20:22

1. 단위 테스트의 목표


소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는 것 - 단위 테스트 -

그림 1.1 테스트가 없는 경우 시작은 유리하지만, 어느 시점부터 급격하게 테스트가 없을 경우 개발 속도가 느려진다.

단위 테스트의 장점

  • 단위테스트는 회귀에 대한 보험을 제공함
    • 안전한 테스트망을 가지고 있기 때문에 코드 베이스에 큰 변화를 줄 자신감을 줌 (Kent Beck)
  • 코드에 대한 변경사항이 기존의 동작을 파괴하지 않도록 보장하는 좋은 방법임 (Martin Fowler)
    • 새로운 기능을 도입하거나 새로운 요구사항에 더 맞게 리팩토링 후 기능이 잘 작동하는지 확인, 확신할 수 있음
  • 조기 버그 발견 가능 (수 초 만에)
  • 테스트 하기 좋은 코드로 설계 개선
  • 문서화의 역할도 함

 

단위 테스트의 단점

  • 초반에 노력이 들어감
    • 장기적으로 보면 그 비용을 메울 수 있음
  • 좋지 않은 테스트 코드를 만들면 유지 보수 비용만 늘어남

 

2. 단위 테스트란 무엇인가


통합 테스트

런던파의 통합 테스트 : 실제 협력자 객체를 사용하는 모든 테스트를 통합 테스트이다. 고전파는 런던파 입장에서 모두 통합 테스트이다.

고전파의 통합 테스트 : 공유 의존성 접근하는 테스트 또는 둘 이상의 동작 단위 검증할 때 통합 테스트이다.

 

단위 테스트의 정의

- 작은 코드 조각을 검증
- 빠르게 수행 가능
- 격리된 방식으로 처리 가능

→ 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트

 

좋은 테스트의 규칙(FIRST)

  1. Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 한다.
  2. Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안된다.
  3. Repeatable: 어느 환경에서도 반복 가능해야 한다.
  4. Self-Validating: 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 자체적으로 검증되어야 한다.
  5. Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.

 

테스트를 바라보는 관점(고전파 VS 런던파)

격리 수준을 어떻게 하느냐에 따라 크게 2가지 관점으로 나뉨

  • 고전파
    • 고전주의적 접근법
    • 테스트 주도 개발(켄트 백, 테스트 주도 개발)
    • 의존성과 테스트 대상 시스템 모두 준비해야 함
  • 런던파
    • 목 추종자
    • 런던 스타일
    • 테스트 대상 시스템(SUT, System Under Test)을 협력자(Collaborator)에게서 격리
    • 클래스 간의 의존성을 줄일 수 있음
    • SUT 에 보다 집중 가능

런던파의 경우, 테스트 대상 시스템의 의존성을 테스트 대역으로 대체 → 테스트 대상 시스템만 검증하는데 집중 가능

 

고전파의 예

@SpringBootTest
@Transactional
class OrderServiceTest {

    @Autowired
    EntityManager em;
    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    public void 상품주문(){
        //given
        Member member = createMember();

        Book book = createBook("시골 JPA", 10000, 10);

        //when
        int orderCount = 2;
        Long orderId = orderService.order(member.getId(), book.getId(), orderCount);

        //then
        Order getOrder = orderRepository.findOne(orderId);

        // 상품 주문시 상태는 ORDER
        assertThat(getOrder.getStatus()).isEqualTo(OrderStatus.ORDER);
        // 주문한 상품 종류 수는 정확해야 한다.
        assertThat(getOrder.getOrderItems().size()).isEqualTo(1);
        // 주문 가격은 가격 * 수량이다.
        assertThat(getOrder.getTotalPrice()).isEqualTo(10000*orderCount);
        // 주문 수량만큼 재고가 줄어야 한다.
        assertThat(book.getStockQuantity()).isEqualTo(8);

    }

여기서 SUT는 상품 주문(order)이고, 협력자는 book 과 member 이다. 대표적인 고전파 방법으로 테스트를 한 예이다.

 

런던파의 예

@DataJpaTest
@ExtendWith(MockitoExtension.class)
@Slf4j
class StudyServiceTest {

    @Mock MemberService memberService;  // 선언적으로 Mock 만드는 방법

    @Mock StudyRepository studyRepository;

    @Test
    void createNewStudy() {

        // Given
        StudyService studyService = new StudyService(memberService, studyRepository);
        assertNotNull(studyService);

        // stub 세팅
        Member member = new Member();
        member.setId(1L);
        member.setEmail("asdf@email.com");

        Study study = new Study(10, "테스트");

        given(memberService.findById(1L)).willReturn(Optional.of(member));
        given(studyRepository.save(study)).willReturn(study);

        // When
        studyService.createNewStudy(1L, study);

        // Then
        assertNotNull(study.getOwnerId());
        assertEquals(member.getId(), study.getOwnerId());

        then(memberService).should(times(1)).notify(study);
        then(memberService).shouldHaveNoInteractions();
    }

}

Mockito 라이브러리를 사용하여 런던파 스타일로 테스트를 구현한 것

 

 

격리 주체 단위 크기 테스트 대역 사용 대상

  격리 주체 단위 크기 테스트 대역 사용 대상
런던파 단위 단일 클래스 불변 의존성 외의 모든 의존성
고전파 단위 테스트 단일 클래스 또는 클래스 세트 공유 의존성

 

통합 테스트

  • 런던파의 통합 테스트 : 실제 협력자 객체를 사용하는 모든 테스트를 통합 테스트이다. 고전파는 런던파 입장에서 모두 통합 테스트이다.
  • 고전파의 통합 테스트 : 공유 의존성 접근하는 테스트 또는 둘 이상의 동작 단위 검증할 때 통합 테스트이다.

 

엔드 투 엔드 테스트(end-to-end test)

엔드 투 엔드 테스트는 공유 의존성뿐만 아니라 조직 내 다른 팀이 개발한 코드 등과 통합해 작동하는 테스트다. 일반 통합 테스트보다 더 많은 의존성이 있다. UI(User Interface), GUI(Graphic User Interface) 테스트라고도 할 수 있다.

엔드 투 엔드 테스트 작성에 가장 많은 비용이 발생하기 때문에 모든 단위 테스트, 통합 테스트 통과한 후 후반에 작성, 실행하는 것이 좋다.

 

3. 단위 테스트 구조


단위 테스트의 구조

  • 크게 AAA 패턴과 Given-When-Then 패턴
    • 준비 (Arrange, Given)
    • 실행 (Act, When)
    • 검증 (Assert, Then)

 

단위 테스트 안티 패턴

  • 테스트 간에 높은 결합도를 가지면 안된다.
    • 테스트를 수정해도 다른 테스트에 영향이 있으면 안됨
  • 준비(Given) 단계에서 생성자로 SUT를 준비하면 테스트 가독성이 떨어짐
    • 테스트만 보고 큰 그림을 보기 힘듦
    • 정적 팩토리 메서드나 빌더 패턴을 이용해서 SUT나 협력자를 준비하자.

 

단위 테스트 명명 지침

  • 엄격한 명명 정책을 따르지 않는다.
    • 표현의 자유를 허용하자
  • 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자.
  • 단어를 밑줄(_)로 구분하자.
    • 가독성이 향상 된다.
  • 유틸리티 코드의 경우 함수 이름, 기댓값을 명명해도 좋다.
  • 좋은 명명 규칙 예
    • Delivery_with_a_past_date_is_valid()
    • Can_detect_an_invalid_delivery_date()

 

4. 좋은 단위 테스트의 4대 요소


 

좋은 단위 테스트에는 다음 4가지 특성이 있음

- 회귀 방지
- 리팩토링 내성
- 빠른 피트백
- 유지 보수성

 

회귀 방지

 

코드를 수정한 후, 기능이 의도한 대로 작동하지 않는 경우를 말함

→ 즉, 코드 수정 후 버그가 있는데도 테스트가 통과되면 안된다.

회귀를 방지하려면 다음 사항을 고려해 봐야 한다.

  • 테스트 중에 실행되는 코드의 양
  • 코드 복잡도
  • 코드의 도메인 유의성

일반적으로 실행되는 코드가 많을수록 회귀가 나타날 가능성이 높음

회귀방지를 극대화하려면 테스트가 가능한 많은 코드를 실행하는 것을 목표로 해야 한다.

 

리팩토링 내성

 

테스트를 실패로 바꾸지 않고 기본 애플리케이션 코드를 리팩토링할 수 있는지에 대한 척도

→ ex) 메서드 이름을 바꾸거나, 코드 조각을 새로운 클래스로 추출하기

  • 거짓 양성
    • 리팩토링으로 기능은 예전과 똑같이 작동하지만 테스트는 실패로 바뀌는 것.

거짓 양성이 적을수록(리팩터링 내성이 높을수록) 좋은 테스트다.

거짓 양성을 줄이려면?

  • 리팩토링 내성을 높이려면 SUT의 구현 세부사항과 테스트 간의 결합도를 낮추면 된다.
    • 이는 해당 구현 세부 사항에서 테스트를 분리하는 것을 말함.

 

 

위 그림을 보면 왼쪽은 SUT를 블랙박스로 취급하고, 식별할 수 있는 동작만 신경쓰고 있다.

 

 

테스트 정확도 지표는 다음 두가지로 구성 됨

  • 테스트가 얼마나 버그 있음을 잘 나타내는가(거짓 음성(False negatives))
  • 테스트가 얼마나 버그 없음을 잘 나타내는가(거짓 양성(False positives))

 

테스트 정확도 = 신호(발견된 버그 수)/소음(허위 경보 발생 수)

 

프로젝트가 성장함에 따라 거짓 양성(False positives)은 테스트 스위트에 점점 더 큰 영향을 미치게 된다.

 

→ 결과적으로, 정기적으로 리팩토링을 진행해줘서 거짓 양성을 낮춰야(리팩토링 내성을 높여야) 한다.

 

테스트 스위트(Test Suite)

  • 특정 테스트 주기에서 실행해야 하는 테스트 케이스(또는 테스트 스크립트)의 집합이나 테스트 절차
  • 테스트 실행 목적에 따라 그룹화된 테스트 케이스의 집합
  • 실행 환경에 따라 구분해 놓은 테스트 케이스의 집합
  • 각 테스트 케이스가 시나리오를 포함하고 있는 경우에 테스트 스위트를 이용한다
  • 테스트 스위트 내 하나의 테스트 케이스가 성공적으로 끝나야 다음 테스트 케이스로 넘어간다
  • 테스트 스위트가 유용하게 쓰이는 테스트 유형 = 빌드 검증 테스트, 스모크 테스트, E2E 테스트, 기능성 검증 테스트, 리그레션 테스트
  • 자동화 리그레션 테스트 케이스 집합 또는 end-to-end 시나리오로 구성된 테스트 케이스 집합을 연속적으로 실행 가능

예) 테스트 스위트 (각 테스트 케이스는 테스트 스크립트를 포함)

  • 테스트 케이스 1: 로그인
  • 테스트 케이스 2: 새로운 제품 추가
  • 테스트 케이스 3: 체크아웃
  • 테스트 케이스 4: 로그아웃
테스트 스크립트 < 테스트 케이스 < 테스트 스위트

 

빠른 피드백과 유지 보수성

 

빠른 피드백은 단위 테스트의 필수 속성이다.

  • 테스트 속도가 빠를수록, 스위트에서 더 많은 테스트를 수행 가능

유지 보수성 지표는 유지비를 평가한다.

  • 테스트가 얼마나 이해하기 어려운가.
  • 테스트가 얼마나 실행하기 어려운가.

 

좋은 단위 테스트를 작성하는 방법

 

좋은 단위 테스트는 처음 세가지(회귀 방지, 리팩토링 내성, 빠른 피드백)은 상호 배타적이다.

  • 세가지 모두 최대로 하는 것은 불가능
  • 셋 중 하나를 희생해야 나머지 둘을 최대로 유지 가능하다.

 

리팩토링 내성을 최대한 많이 갖는 것을 목표로 해야 한다. 따라서 테스트가 얼마나 버그를 잘 찾는지(회귀 방지)와 얼마나 빠른지(빠른 피드백) 사이에 선택으로 절충된다.

 

블랙박스 테스트와 화이트박스 테스트 간의 선택

  • 블랙박스 테스트
    • 시스템의 내부 구조를 몰라도 시스템 기능을 검사하는 것
    • 일반적으로 명세와 요구 사항, 애플리케이션이 무엇을 해야 하는지를 중심으로 구축
  • 화이트박스 테스트
    • 애플리케이션 내부 작업을 검증
    • 소스 코드를 검사

회귀 방지 리팩토링 내성

화이트박스 테스트 좋음 나쁨
블랙박스 테스트 나쁨 좋음

 

리팩토링 내성이 좋은 블랙박스 테스트를 기본 전략으로 선택해야 한다.

 

5. 목과 테스트의 취약성


 

목과 스텁 구분

테스트 대역은 목과 스텁으로 구분

  • 목(Mock)
    • 외부로 나가는 상호 작용을 모방하고 검사하는데 도움
    • SUT가 상태를 변경하기 위한 의존성을 호출
    • ex) 이메일 발송
  • 스텁(Stub)
    • 내부로 들어오는 상호작용을 모방하는데만 도움
    • SUT가 입력 데이터를 얻기 위한 의존성을 호출
    • ex) DB로부터 데이터 검색

테스트 대역은 크게 목과 스텁으로 나뉜다.

 

 

목과 스텁의 구분

 

도구로서의 목과 테스트 대역으로서의 목을 구분하자

  • 도구로서 목은 목 라이브러리(ex)Mockito)을 말함
  • 테스트 대역으로의 목은 위에서 말했던 외부로 나가는 상호 작용을 위한 목

 

목과 스텁을 함께 쓸수도 있다.

  • Mockito의 경우 둘 다 사용
@DataJpaTest
@ExtendWith(MockitoExtension.class)
@Slf4j
class StudyServiceTest {

    @Mock MemberService memberService;  // 선언적으로 Mock 만드는 방법

    @Mock StudyRepository studyRepository;

    @Test
    void createNewStudy() {

        // Given
        StudyService studyService = new StudyService(memberService, studyRepository);
        assertNotNull(studyService);

        // stub 세팅
        Member member = new Member();
        member.setId(1L);
        member.setEmail("asdf@email.com");

        Study study = new Study(10, "테스트");

        given(memberService.findById(1L)).willReturn(Optional.of(member));
        given(studyRepository.save(study)).willReturn(study);

        // When
        studyService.createNewStudy(1L, study);

        // Then
        assertNotNull(study.getOwnerId());
        assertEquals(member.getId(), study.getOwnerId());

        then(memberService).should(times(1)).notify(study);
        then(memberService).shouldHaveNoInteractions();
    }

}
given절에서 테스트 대역에 스텁을 세팅하고, 마지막 then절에서 목을 사용하여 SUT를 검증한다.

 

  • 최종 결과가 아닌 사항을 검증하는 것
    • ex) 스텁과의 상호작용을 검증하는 것
스텁과의 **상호 작용을 검증하는 것**은 일반적으로 안티 패턴이다.

 

CQS(Command Query Separation, 명령 조회 분리)

  • 명령(command)은 부작용을 일으키고 사이드 이펙트를 일으키고, 반환 값이 없는 메서드(return void)
    • 목(mock)에 해당
  • 조회(Query)는 사이드 이펙트가 없고, 값을 반환하는 것
    • 질문을 할때 답이 달라져서는 안된다.
    • 스텁(stub)에 해당

 

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

테스트는 ‘어떻게’가 아니라 ‘무엇’에 중점을 둬야 한다.

  • 모든 제품 코드는 2차원으로 분류 가능
    • 공개 API or 비공개 API
    • 식별할 수 있는 동작 or 구현 세부 사항
  • 잘 설계된 API는 식별할 수 있는 동작은 공개 API와 일치, 모든 구현 세부 사항은 비공개 API 뒤에 숨어 있음
  • 연산: 계산을 수행하거나 부작용을 초래하는 메서드
    • 하나의 기능에 하나의 연산이 있어야 한다. 하나의 기능에 여러 연산을 사용하면 응집도가 떨어짐
  • 상태: 시스템의 현재 상태
  • 잘 설계된 API → 단위 테스트 품질도 자동으로 올라감

 

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

시스템 내 통신을 검증하고자 목을 사용하면 취약한 테스트로 이어짐

  • 통신과 해당 통신의 사이드 이펙트가 외부 환경에서 보일 때만 목을 사용해야 함
  • 시스템 내 통신을 검증하고자 하는 경우에는 되도록이면 목을 사용하지 말자.

 

6. 단위 테스트 스타일


단위 테스트의 세가지 스타일

단위 테스트는 세가지 스타일이 있다.

  • 출력 기반 테스트
  • 상태 기반 테스트
  • 통신 기반 테스트

 

출력 기반 테스트

  • 테스트 대상 시스템(SUT)에 입력을 넣고, 생성되는 출력을 점검하는 방식
  • 반환 값만 검증하면 됨
  • 사이드 이펙트가 거의 없음
  • 함수형이라고도 함
  • 거짓 양성 방지가 가장 우수함

 

상태 기반 테스트

  • 작업이 완료된 후 시스템 상태를 확인하는 방식
  • 여기서 상태란 SUT나 협력자 중 하나, 혹은 DB나 파일 시스템과 같은 프로세스 외부 의존성의 상태를 의미

 

통신 기반 테스트

  • 협력자 간의 통신을 검증하는 방식
  • 허위 경보에 가장 취약함

세가지 스타일 중 출력 기반 테스트를 선호해라!

 

출력 기반 테스트 >>>>> 상태 기반 테스트 >>>>>>>>>>>>>>>>>>>>>>>>>> 통신 기반 테스트

 

 

 

7. 가치 있는 단위 테스트를 위한 리팩토링


리팩토링할 코드 식별하기

 

모든 제품 코드는 2차원의로 분류가 가능

  • 복잡도 또는 도메인 유의성
  • 협력자 수

코드 복잡도

  • 코드 복잡도는 코드 내 의사 결정(분기) 지점 수가 클수록 → 복잡도 커진다.

도메인 유의성

  • 코드가 프로젝트의 문제 도메인에 대해 얼마나 의미 있는지
  • 일반적으로 도메인 계층(data aceess Layer)의 모든 코드는 최종 사용자의 목표가 직접적인 연관성이 있음 → 도메인 유의성이 높다
  • 유틸리티 코드의 경우 연관성이 거의 없다.
  • 복잡한 코드와 도메인 유의성을 갖는 코드가 단위 테스트에 가장 이롭다.
    • 회귀 방지에 뛰어나기 때문!

 

코드의 4가지 유형

 

  • 도메인 모델과 알고리즘
    • 보통 복잡한 코드는 도메인 모델 but 100%는 아님
  • 간단한 코드
    • 협력자가 없이 코드가 간단한 경우
    • 테스트할 가치가 0에 가깝다.
  • 컨트롤러
    • 복잡하거나 비즈니스에 중요한 작업을 하는 것이 아니라 도메인 클래스와 외부 애플리케이션 같은 다른 구성 요소의 작업을 조정
  • 지나치게 복잡한 코드
    • 협력자가 많고 복잡할 경우

코드의 4가지 유형의 상관관계, 지나치게 복잡한 코드는 최대한 지양해야 한다. (https://www.kimcoder.io/books/unit-testing/7)

 

지나치게 복잡한 코드를 벗어나 도메인 모델과 알고리즘만 단위 테스트하는 것이 매우 가치는 있는 테스트임

 

험블 객체

 

지나치게 복잡한 코드를 쪼개려면 험블 객체 패턴를 이용해야 한다.

  • 테스트 대상 코드의 로직을 테스트하려면, 테스트가 가능한 부분을 추출해야 한다.
  • 결과적으로 코드는 테스트 가능한 부분을 둘러싼 얇은 험블 래퍼(humble wrapper)가 된다.
  • 이 험블 래퍼가 테스트하기 어려운 의존성과 새로 유출된 구성 요소를 붙임
  • 자체적인 로직이 거의 없거나 전혀 없으므로 테스트할 필요가 없음
  • 단일 책임 원칙을 지키면 험블 객체 패턴을 가질 수 있음

 

8. 통합 테스트를 하는 이유


통합테스트?

  • 통합 테스트는 테스트 스위트에서 중요한 역할을 함
  • 단위 테스트 개수와 통합 테스트의 균형을 맞추는 것도 중요함
  • 단위 테스트가 아닌 모든 테스트를 통합 테스트에 해당함
  • 통합 테스트는 회귀 방지와 리팩터링 내성이 우수하고, 단위 테스트는 유지 보수성과 피드백 속도가 우수하다.

통합 테스트는 주요 흐름(happy path)과 단위 테스트가 다루지 못하는 예외 상황(edge case)을 다룬다.

  • 비즈니스 시나리오당 하나 or 두 개가 있으면 시스템 전체의 정확도를 보장해 준다.

코드의 4가지 유형에서 도메인 모델 및 알고리즘은 단위 테스트에, 컨트롤러는 통합 테스트에 해당함

 

 

테스트 피라미드를 통해 다시 보는 테스트 수와 테스트 종류와의 관계

 

좋지 않은 테스트를 작성하는 것보다 차라리 테스트를 작성하지 않는 것이 좋다.

 

어떤 프로세스 외부 의존성을 직접 테스트해야 하는가?

 

외부 의존성을 테스트하는 방법은 직접 사용하거나 목으로 대체하는 방법이 있음

외부 의존성은 크게 두가지 범주로 나뉨

  • 관리 의존성(전체를 제어할 수 있는)
    • 애플리케이션을 통해서만 접근이 가능
    • 해당 의존성과의 상호 작용은 외부 환경에서 볼 수 없음
    • 대표적으로 DB가 있음
  • 비관리 의존성(전체를 제어할 수 없는)
    • 해당 의존성과의 상호작용은 외부에서 볼 수 있음
    • SMTP 서버나 메시지 버스 등

 

관리 의존성은 실제 인스턴스를 사용하고, 비관리 의존성은 목으로 대체하자.

 

 

9. 목 처리에 대한 모범 사례


목의 가치를 극대화하기

 

비관리 의존성에만 목을 사용하는 것은 목의 가치를 극대화 하는 첫번째 단계일 뿐이다.

 

목을 사용할 때는 꼭 시스템 끝에서 비관리 의존성과의 상호 작용을 검증하라!!

 

IBus 는 시스템 끝에 있다. IMessgeBus는 컨트롤러와 메시지 버스 타입 사슬에서 중간 고리일 뿐이다. IMessageBus 대신 IBus를 목으로 처리하면  회귀 방지가 좋아진다 .

 

목 처리에 대한 모범 사례

 

  • 비관리 의존성(외부 시스템)에만 목 적용하기
  • 시스템 끝에 있는 의존성에 대해 상호 작용 검증하기
  • 목은 통합 테스트만을 위한 것이다.
    • 도메인 모델(persistance Layer) 은 단위 테스트 범주에 속하고, 컨트롤러를 다루는 테스트(presentation Layer)는 통합 테스트다.
  • 테스트당 목이 하나일 필요는 없다.
    • 호출 횟수 검증하기
      • 예상하는 호출이 있는가?
      • 예상치 못한 호출이 있는가?
messageBusMock.Verfiy(x => x.SendEmailChangeMessage(user.UserId, "new@gmail.com"));
 // 메시지를 전송하는지 확인하는 것만으로 충분하지 않다.
 
 // 아래와 같이 메시지가 정확히 한 번만 전송되는지 확인해야 한다.
 messageBusMock.Verfiy(x => x.SendEmailChangeMessage(user.UserId, "new@gmail.com"), Times.Once);  // <- 해당 메서드를 한 번만 호출하는지 확인

 

보유 타입만 목으로 처리하기

 

서드파티 라이브러리 위에 항상 어댑터를 작성하고, 기본 타입 대신 해당 어댑터를 목으로 처리해야 한다.

  • 서드파티 코드의 작동 방식에 대해 깊이 이해하지 못한 경우가 많다.
  • 서드파티 코드의 기술 세부 사항까지는 꼭 필요하지 않기에 어댑터는 이를 추상화하고, 애플리케이션 관점에서 라이브러리와의 관계를 정의한다.

실제로 어댑터는 코드와 외부 환경 사이에 손상 방지 계층으로 작동한다.

  • 기본 라이브러리의 복잡성을 추상화하고
  • 라이브러리에서 필요한 기능만 노출하며
  • 프로젝트 도메인 언어를 사용해 수행할 수 있다.
  • 또한 이렇게 추상 계층을 중간에 둠으로써 이러한 파급 효과를 하나의 클래스(어댑터)로 제한할 수 있다.

 

10. 데이터베이스 테스트


DB 테스트를 위한 전제 조건

통합테스트에서는 관리 의존성이 그대로 있어야 한다.

  • 형상 관리 시스템에서 DB 유지
  • 모든 개발자를 위한 별도의 DB 인스턴스 사용
  • DB 배포에 마이그레이션 기반 방식 작용
참조 데이터는 애플리케이션이 제대로 작동하도록 미리 채워야 하는 데이터다.

 

인메모리 DB는 일반 DB와 기능적으로 일관성이 없기 때문에 사용하지 않는 것이 좋다.

  • 또한 운영 환경과 테스트 환경이 일치하지 않는 문제가 발생한다.
  • 일반 DB와 인메모리 DB의 차이로 인해 테스트에서 거짓 양성 또는 거짓 음성이 발생하기 쉽다.

 

쓰기를 철저히 테스트 하는 것이 중요하다.

  • 위험성이 높기 때문
  • 쓰기 작업이 잘못되면 데이터가 손상돼 DB뿐만 아니라 외부 애플리케이션에도 영향을 미칠 수 있다.
  • 그러나 읽기는 해당하지 않는다.
  • 따라서 읽기 테스트 임계치는 쓰기 테스트 임계치보다 높아야 한다.

 

11. 단위 테스트 안티 패턴


비공개 메서드 단위 테스트

단위 테스트를 하려고 비공개 메서드를 노출하면 기본 원칙 중 하나인 식별할 수 있는 동작만 테스트하는 것을 위반한다.

  • 비공개 메서드를 직접 테스트하는 대신, 포괄적인 식별할 수 있는 동작으로서 간접적으로 테스트하는 것이 좋다.

비공개 메서드를 절대 테스트하지 말라는 규칙에도 예외가 있다.

  • 예외를 이해하려면 코드의 공개 여부와 목적 간의 관계를 다시 살펴봐야 한다.

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

공개 좋음 나쁨
비공개 해당 없음 좋음

테스트로 유출된 도메인 지식

도메인 지식을 테스트로 유출하는 것 또한 흔한 안티 패턴임

  • 보통 복잡한 알고리즘을 다루는 테스트에서 일어난다.
테스트를 작성할 때는 특정 구현을 암시하지 말라.

단위 테스트에서는 예상 결과를 하드코딩하는 것이 좋다.

코드 오염

코드 오염은 테스트에만 필요한 코드가 프로덕션 코드에 추가하는 것을 말한다.

  • 코드 오염의 문제는 테스트 코드와 제품 코드가 혼재돼 유지비가 증가하는 것이다.

구체 클래스를 목으로 처리하기

구체 클래스 대신 목으로 처리해서 본래 클래스의 기능 일부를 보존할 수 있으며, 때때로 유용하다.

  • 그러나 이러한 대안은 단일 책임 원칙을 위배한다.

 

참조

  • 단위테스트(블라디미르 크리코프)