logo
Search검색어를 포함하는 게시물들이 최신순으로 표시됩니다.
    Table of Contents
    7장: 신뢰할 수 있는 테스트

    이미지 보기

    7장: 신뢰할 수 있는 테스트

    테스트를 신뢰할 수 있는지 판단하는 방법과 신뢰도가 낮은 테스트들

    • 25.03.24 작성

    • 읽는 데 17

    TOC

    참고

    본 내용은 단위 테스트의 기술을 읽고 정리한 내용입니다.
    책의 내용과 함께 개인적인 의견과 생각, 학습을 담아 작성하였습니다.

    좋은 테스트의 특성

    • 신뢰성
      • 신뢰할 수 있다 = 버그가 없고 올바른 대상을 테스트한다.
      • 좋은 단위 테스트의 첫 번째 기준
      • 신뢰할 수 없다면 실행하는 의미가 없다.
    • 유지 보수성
      • 코드가 조금만 바뀌어도 테스트를 수정해야 한다면 유지보수에 지쳐 손을 놓는다.
    • 가독성
      • 테스트가 잘못된 경우 문제를 파악할 수 있는 능력

    테스트를 신뢰할 수 있는지 판단하는 방법

    테스트를 신뢰하지 않는 상황

    • 거짓 양성 : 테스트는 실패했지만 신경 쓰지 않음
    • 거짓 음성 : 테스트가 통과했지만 의심스러운 경우
    • 테스트가 가끔 통과할 때
    • 테스트가 현재 작업과 관련 없다고 생각할 때
    • 테스트에 버그가 있다고 느낄 때

    테스트를 신뢰할 수 있는지 판단하는 방법

    • 테스트가 실패했을 때 쉽게 넘어가지 않을 때
    • 테스트가 통과한 뒤, 따로 수동으로 테스트하거나 디버깅하지 않을 때

    테스트가 실패하는 이유

    • 프로덕션 코드에서 실제 버그가 발견된 경우
    • 테스트가 거짓실패를 일으키는 경우
    • 기능 변경으로 테스트가 최신 상태가 아닌 경우
    • 테스트가 다른 테스트와 충돌하는 경우
    • 테스트가 불안정한 경우

    테스트가 거짓 실패를 일으키는 경우

    • 테스트 자체에 버그가 있는 경우
    • 잘못된 테스트를 가볍게 보지 말라.
    • 테스트가 통과되더라도 실제로는 문제가 있는 상황을 발견하지 못할 수도 있다.

    테스트에 버그가 있는지 찾아내는 방법

    다음 항목에 해당하면 거짓 실패

    • 잘못된 항목이나 잘못된 종료점을 검증하는 경우
    • 잘못된 값을 진입점에 전달하는 경우
    • 진입점을 잘못 호출하는 경우

    테스트 버그를 발견했을 때 해야 할 일

    • 버그를 수정하고 테스트를 재실행한다.
    • 통과했으면 일부러 버그를 넣어보자.
    • 실패하지 않는다면 여전히 테스트에 문제가 있을 수 있다.
    • 디버깅을 계속해도 실패한다면 프로덕션 코드에 문제가 있을 수 있다.
    • 오히려 테스트를 통해 버그를 찾아낸 좋은 일

    잘못된 테스트를 방지하는 방법

    • 테스트 주도 개발(TDD) 방식으로 코드를 작성한다.
    • 테스트가 실패해야 정상
    • 그리고 이를 통과하도록 만든다.
    • 처음부터 통과하면 테스트가 잘못된 거고, 계속 실패하면 테스트가 잘못된거다.

    기능 변경으로 테스트가 최신 상태가 아닌 경우

    두 가지 선택지가 있다.

    • 테스트를 새로운 기능에 맞게 수정
    • 새로운 기능을 대상으로 새 테스트 만들고 기존 테스트 삭제

    아마 가장 많이 접하게 되는 테스트 실패의 원인일 것 같아요. 너무 빡빡하지 않으면서도 비즈니스적으로 중요한 테스트들을 짜는 연습을 해볼 필요가 있겠네요.

    단위 테스트에서 불필요한 로직 제거

    • 복잡해진 테스트는 디버그와 코드 검증에 더 많은 시간을 소모하게 한다.
    • 테스트 코드에 복잡한 로직이 포함되면 그만큼 테스트 자체에 버그가 생길 확률이 높다.
    • 실패한 테스트가 있으면 테스트 코드 자체의 로직을 점검하는 것도 중요하다.

    단위 테스트에서 줄이거나 없애야 할 코드

    • switch, if, else 문
    • foreach, for, while 루프
    • 문자열 연결(+ 기호) 등
    • try/catch 블록

    Assert문에서 로직: 동적 기댓값 생성

    하지 마라.

    • 테스트 대상과 기댓값을 같은 동적인 방식으로 처리하면 틀려도 동일한 버그를 발생시킨다.
    • 즉, 정상으로 통과하게 되는데, 이건 신뢰성 문제이다.
    • 테스트는 항상 실제 코드와는 다른 방식으로 기댓값을 설정해야 한다.

    검증 단계에서 기댓값을 동적으로 생성하지 말고 되도록 하드코딩된 값을 사용하라.

    테스트가 복잡해지면 생기는 문제들

    • 테스트를 읽고 이해하기가 어렵다.
    • 테스트를 재현하기가 어렵다.
    • 테스트에 버그가 있을 가능성이 높아지거나 잘못된 것을 검증할 수 있다.
    • 테스트가 여러 일을 하므로 이름 짓기가 어려워진다.

    복잡한 테스트를 만들거라면?

    간단한 테스트와의 격리

    • 기존의 간단한 테스트를 대체하지 말고 새로운 테스트로 추가하라.
    • 단위 테스트가 아닌 다른 테스트를 포함하도록 특정 프로젝트나 폴더에 포함시킨다.
    • 복잡한 테스트는 필요할 때만 사용할 수 있게 된다.

    테스트가 통과하더라도 끝이 아니다

    잘못된 신뢰(false trust)

    • 신뢰하지 말아야 할 테스트를 신뢰
    • 하지만, 그 사실을 아직 모르는 상태
    • 테스트를 검토하고 잘못된 신뢰를 찾아내는 것은 중요

    테스트를 믿지 못하는 이유들

    • 검증 부분이 없다.
    • 테스트를 이해할 수 없다.
    • 단위 테스트가 불안정한 통합 테스트에 섞여 있다.
    • 테스트가 여러 가지를 한꺼번에 검증한다.
    • 테스트가 자주 변경된다.

    테스트를 이해할 수 없는 경우

    • 이름이 적절하지 않은 테스트
    • 코드가너무 길거나 복잡한 테스트
    • 변수 이름이 헷갈리게 되어 있는 테스트
    • 숨어 있는 로직이나 이해하기 어려운 가정을 포함한 테스트
    • 결과가 불분명한 테스트(실패도 아니고 통과도 아닌 경우)
    • 충분한 정보를 제공하지 않는 테스트 메시지

    이해하지 못하면 그 테스트를 신경 써야 하는지 여부조차 알 수 없다.

    단위 테스트가 불안정한 통합 테스트와 섞여 있는 경우

    • 테스트의 혼재는 불안정한 테스트를 묵인하고 넘어가기 좋은 환경
    • 불안정하다고 탓할 여지를 아예 없애는 게 낫다.
    • 안정적인 테스트 영역(safe green zone)을 만들어라.
      • 빠르고 신뢰할 수 있는 테스트만 포함한다.
      • 네임스페이스와 폴더의 모든 테스트를 실행할 수 있어야 한다.
      • 모든 테스트가 통과할 것이라는 믿음이 있어야 한다.

    테스트가 여러 가지를 한꺼번에 검증하는 경우

    describe('trigger', () => {
      it ('works', () => {
        const callback = jest.fn();
        const result = trigger(1, 2, callback);
        expect(result).toBe(3);
        expect(callback).toHaveBeenCalledWith("I'm triggered");
      })
    })
    

    이 테스트는 두 가지를 검증하고 있다.

    • 테스트 이름이 모호해진다.
      • 테스트 이름을 잘 짓는 것은 디버깅과 문서화에 매우 중요하다.
      • 하나의 관심사만 테스트할 때는 이름 짓기가 쉽다.
    • 각 검증은 별도의 테스트로 나누어서 실행하여 무엇이 실제로 실패했는지 명확히 확인하는 것이 좋다.
    describe('trigger', () => {
      it ('works', () => {
        const callback = jest.fn();
        trigger(1, 2, callback);
        expect(callback).toHaveBeenCalledWith("I'm triggered");
      })
    
      if ("sums up given values", () => {
        const result = trigger(1, 2, jest.fn());
        expect(result).toBe(3);
      })
    })
    

    테스트를 나눌 때 고려해야 할 점
    첫 번째 검증이 실패했을 때, 다음 검증 결과가 여전히 중요하다면 각 검증을 서로 다른 테스트 2개로 독립적으로 진행하는 것이 좋다.

    테스트가 자주 변경되는 경우

    • 현재 날짜와 시간을 사용하는 테스트
    • 난수, 컴퓨터 이름, 외부 환경 변수 값을 가져오는 테스트
    • 동적으로 만든 값을 사용하는 경우

    불안정한 테스트 다루기

    코드에 변화가 없는데 일관성 없는 결과를 반환하는 테스트

    테스트 수준에 따른 테스트 분류

    • 테스트 수준이 낮은 단위 테스트, 컴포넌트 테스트
      • 테스트가 모든 의존성을 완전히 제어할 수 있다.
      • 따라서 변동 요소가 없다.
      • 모드 실행을 완전히 예측할 수 있다.
    • 테스트 수준이 높은 API 테스트, 통합 테스트
      • 스텁과 모의 객체를 덜 사용하고
      • DB, 네트워크, 환경 설정 등 실제 의존성을 더 많이 사용
      • 제어할 수 없는 변동 요소가 많아진다.
    • 최상위 수준의 테스트 E2E 테스트
      • 모든 의존성을 실제로 사용
      • 서드 파티, 보안 및 네트워크 계층, 환경 설정 등

    불안정한 테스트를 발견했을 때 할 수 있는 일관성

    불안정한 테스트를 없애는 것은 장기 목표로 삼아야 한다.

    문제 정의하기

    • '불안정'이 무엇을 의미하는지 명확히 정의하기
    • 불안정하다고 판단된 테스트는 별도로 모아 실행한다.
    • 불안정한 테스트를 하나씩 검토하며 수정, 리팩터링, 삭제 과정을 밟자.

    수정

    • 가능한 의존성을 제어하여 테스트 안정성을 높인다.
    • ex. DB에 특정 데이터가 필요한 경우, 테스트 실행 시 데이터 삽입

    리팩터링

    • 의존성을 제거하거나 제어
    • 더 낮은 수준의 테스트로 변환
    • 불안정성 제거
    • ex. 실제 서버 대신 스텁을 사용하여 네트워크 요청

    삭제

    • 테스트로 얻는 이점이 유지 보수 비용을 감당하고도 남는지 검토
    • 오래되고 불안정한 테스트는 차라리 없애는 것이 나을 수도 있다.
    • 오래되고 쓸모 없는 테스트를 그대로 놔두는 것은 비용이다.

    상위 수준의 테스트에서 안전성을 유지하는 방법

    어떤 환경에서도 테스트가 반복적으로 실행될 수 있도록 하는 것이 중요하다.

    • 테스트가 데이터베이스나 네트워크 서비스 같은 외부 시스템을 변경했으면 변경한 내용을 롤백한다.
    • 다른 테스트가 외부 시스템의 상태를 변경하지 않도록 한다.
    • 외부 시스템과 의존성을 제어할 수 있어야 한다.
      • 해당 시스템을 언제든지 다시 만들 수 있게 하기
      • 제어 가능한 더미 시스템을 만들기
      • 테스트 전용 계정을 만들어 안전하게 관리하기

    외부 시스템을 다른 회사에서 관리하는 경우 외부 의존성을 제어하기가 힘들거나 불가능할 수 있다. 이 상황에서는 다음 방법을 고려해 볼 수 있다.

    • 저수준 테스트가 이미 특정 기능이나 동작을 검증하고 있다면 일부 고수준 테스트를 삭제한다.
    • 일부 고수준 테스트를 저수준 테스트로 바꾼다.
    • 새로운 테스트를 작성할 때는 배포 파이프라

    총평

    기억하고 싶은 내용들

    • 테스트를 신뢰할 수 없다면, 실행하는 의미가 없다.
    • 검증 단계에서 기댓값은 최대한 하드코딩된 값 사용하기(틀려도 같이 틀려서 정상 처리됨)
    • 테스트로 얻는 이점이 유지 보수 비용을 감당하고도 남는지 검토해볼 것.
      • 오래되고 불안정한 테스트는 차라리 없애는 것이 나을 수도 있다.

    공감하는 내용

    본 장에서는 이름처럼 신뢰할 수 있는 테스트에 완전한 초점을 맞춘 것이 인상적이었습니다. 확실히 테스트는 그 결과를 신뢰할 수 있어야 의미가 있다는 저자의 의견에 적극 공감합니다.

    따라하기 힘들 것 같은데

    사실 말이 쉬운 것 같은 요소들이 많습니다. 통과한 테스트의 인자를 일부 고정해 일부러 틀리게 한다거나, 수준을 오르내리며 테스트를 쪼개고 붙이는 것들, 그리고 디렉토리를 옮겨 나누는 등 다양한 솔루션을 제안했는데요, 그런데 현실적으로는 조금 어렵지 않을까 싶었던 요소였습니다. 실력자들은 이런 걸 쉽게 할까요?

    profile

    FE Developer 박승훈

    노력하는 자는 즐기는 자를 이길 수 없다