logo
Search검색어를 포함하는 게시물들이 최신순으로 표시됩니다.
    Table of Contents
    8장: 유지 보수성

    이미지 보기

    8장: 유지 보수성

    테스트가 실패하는 근본 원인과 유지 보수성 강화

    • 25.03.29 작성

    • 읽는 데 10

    TOC

    참고

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

    코드 스멜

    • 유지 보수성이 테스트를 얼마나 자주 변경해야 하는지 가늠하는 척도라면 그 횟수를 최소화하는 것이 중요
    • 유지보수의 원인을 파악할 때 확인해볼 것
      • 언제 테스트가 실패하여 변경이 필요하다는 사실을 깨닫는가?
      • 왜 테스트가 실패하는가?
      • 어떤 테스트가 실패했을 때 테스트 코드를 반드시 수정해야 하는가?
      • 테스트 코드를 반드시 수정할 필요가 없더라도 언제 코드를 변경하는가?

    테스트 실패로 코드 변경

    • 실제 실패(true failure) : 실제 버그로 발생한 실패
    • 가짜 실패(false failure) : 버그가 아니지만 테스트가 실패한 경우
    • 테스트의 유지 보수성 : 거짓 실패 횟수와 원인 분석이 필요
      • 테스트 자체에 버그가 있는 경우
      • 다른 이유들을 아래에서 정리

    테스트가 관련이 없거나 다른 테스트와 충돌하는 경우

    • 코드에 새로운 기능을 넣은 경우
    • 새 기능이 추가된 코드의 신규 요구 사항을 제대로 반영했는지 확인하는 용도로 확인 가능
    • 요구사항의 변경에 의해 실패한다면 유효하지 않은 테스트는 삭제 가능
    • 예외 : 기능 토글(feature toggle)을 사용하는 경우

    프로덕션 코드의 API 변경

    • 테스트 대상 코드가 변경되어 함수나 객체 사용 방식이 달라지는 경우

    • 이 경우는 가능한 피해야 한다.

    • 해결방안 : 팩토리 함수로 객체를 만드는 과정을 분리

      • 변경사항을 중앙에서 한 번만 처리하면 된다.
    • 저의 의견 : 모든 테스트에 대해 팩토리 함수를 만들 수는 없다보니, 어떤 경계에서 팩토리 함수로 변경을 할 것인지에 대한 기준이 필요할 것 같다고 느낍니다. 예를 들면, 같은 stub 객체를 2번 이상 중복으로 사용한다거나, 자주 바뀌는 대상 코드여서 계속 변경이 됨을 느낀다던지 하는 것이죠. 부딪혀보며 기준을 잡아가야 할 부분 같네요.

    다른 테스트가 변경되었을 경우

    • 테스트가 독립적이지 않고 서로 영향을 줄 때
    • 중요 : 테스트는 항상 다른 테스트와 독립적으로 실행되어야 한다.
    • 같은 기능을 검증하더라도 각 테스트는 서로 격리된 공간에서 실행되어야 한다.
    • 특징 : 순서가 정해진 테스트
      • 특정 테스트가 이전 테스트의 실행 여부에 따라 결과가 달라지는 상황
      • 다른 테스트의 설정값, 또는 공유 변수 등 자원의 상태값 변경에 영향을 받는 경우
    • 해결방안 : 테스트를 독립적으로 만들기
      • (추천) 해당 테스트를 동작시키는 준비 과정을 담당하는 헬퍼 함수를 만든다.
      • 테스트 사이에 공유 변수 및 캐시를 초기화한다. (beforeEach 또는 afterEach)

    유지 보수성을 높이는 리팩터링 방법

    지금부터는 테스트가 실패하지 않도록 유지보수성을 보장하는 방법

    private 또는 protected 메서드 사용하지 않기

    • private/protected : 메서드에 대한 접근을 제한하는 용도로 사용
    • 보통 이 메서드를 호출하는 public 메서드가 있다.
    • 항상 외부 인터페이스로 시작하는 더 큰 작업의 일부
    • public 메서드를 잘못 쓰고 있으면 무용지물
    • 해결방안
      • public 메서드로 private 메서드를 간접 테스트
      • 혹은 메서드 자체를 public/static으로 변경
      • 메서드를 새로운 클래스나 모듈로 변경

    테스트에서도 DRY 원칙 고수

    DRY 원칙 : Don't Repeat Yourself

    • DRY 원칙은 프로덕션 코드 뿐만 아니라 테스트 코드에도 적용되어야 한다.
    • 과유불급 : 중복을 제거하는 것이 지나치면 가독성을 해칠 수도 있다.

    초기화 함수를 사용하지 않기

    • 초기화 함수는 잘못 사용되기 쉽다.
      • 원래 의도와 다르게 사용할 가능성이 높다.
      • 파일 내 일부 테스트에서만 사용하는 객체를 초기화 함수에서 초기화한다.
      • 길고 이해하기 어려운 초기화 코드를 작성한다.
      • 초기화 함수 내에서 목이나 가짜 객체를 만든다.
    • 초기화 함수는 한계가 있다.
      • 객체를 초기화할 때에만 유용하다.
      • 매개변수나 반환 값을 가질 수 없다.
      • 팩토리 메서드로 사용할 수 없다.
    • 해결방안
      • 헬퍼 함수를 사용한다.
      • 팩토리 메서드를 사용한다.

    매개변수화된 테스트로 중복 코드 제거

    • jest에서는 test.each() 또는 it.each() 함수 사용
    • 원래 beforeEach() 블록에 있어야 할 설정 로직을 테스트 준비(arrange) 단계로 옮길 수 있다.
    describe('sum with parameterized tests', () => {
      test.each([
        ['1', 1],
        ['2', 2],
      ])('add %s, returns that number', (input, expected) => {
        const result = sum(input);
        expect(result).toBe(expected);
      });
    });
    

    저의 의견 : 한 테스트에서 초기화함수를 제거할 수 있는 것은 맞지만... 아예 다른 describe 문에서는 사용하기가 어려울 것 같네요. 국소적 해결방안 같습니다.

    과잉 명세된 테스트

    • 코드 내부가 어떻게 구현되어야 하는지까지 검증하는 테스트
      • 메서드의 내부 로직이나 특정 상태 변화를 검증하는 테스트
      • 코드 변경에 따른 테스트의 잦은 수정 야기
    • 과잉 명세된 테스트의 조건
      • 테스트의 객체가 내부 상태만 검증
      • 테스트가 목을 여러 개 만들어 사용
      • 테스트가 스텁을 목처럼 사용
      • 테스트가 필요하지 않은데도 특정 실행 순서나 정확한 문자열 비교를 포함

    목을 사용한 내부 동작 과잉 명세

    • 클래스나 모듈의 내부 함수가 호출되었는지만 검증
    • 정작 작업 단위의 종료점은 확인 안 함
    • 테스트를 위한 테스트일 뿐
    • 해결방안
      • 메서드가 값을 반환한다면 그 메서드를 모의 함수로 만들지 말라.
      • 종료점을 찾아라. 단, 수행하고자 하는 테스트 종류에 따라 달라질 수 있다.
      • (추천) 값 기반 테스트 : 호출된 함수의 반환 값 확인
      • 상태 기반 테스트 : 진입점 함수와 같은 레벨의 형제 함수나 속성 확인
      • 서드 파티 테스트 : 목을 사용해야 하고, 코드 내부에서 fire-and-forget 위치를 찾아야 한다.

    결과와 순서를 지나치게 세밀하게 검증

    • 검증을 여러 작은 검증 구문으로 나누어 각각의 측면을 명확하게 확인하는 것이 좋다.
    • 검증의 결과를 순서와 관련이 없도록 쪼개고, 결합을 분리하라.
    • 문자열은 '완전 일치'가 아닌, 비즈니스 코어를 담은 문자열을 '포함'하는지만 확인하라.
    profile

    FE Developer 박승훈

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