[TDD/방법론] Testable Code란?

nana·2024년 9월 2일

방법론

목록 보기
3/9

해당 포스팅은 이 아티클을 번역 + 추가 정보를 덧붙였습니다.

Testable Code?

유닛 테스트를 하다보면 테스트가 자주 깨지는 경험을 한다.
조금만 바꿔도 민감하게 실패하는 코드가 좋은 테스트코드라지만, 변경한 곳과 상관 없는 부분에서 실패코드가 발생한다면 테스트 코드가 잘못된 것은 아닌지 점검해 볼 필요가 있다.

Overview of Testability in Code(코드의 테스트 가능성 개요)

  • 코드에서 '테스트가능성'은 얼마나 소프트웨어 시스템을 쉽게 테스트 할 수 있는지를 나타냄.
  • 테스트 가능한 코드의 주요 특성은 코드가 이해가능하고 목적이 명확하다는 것을 의미함.
  • 외부 시스템이나 상태에 의존하지 않고 코드 단위를 개별적으로 테스트할 수 있는 독립성을 지님.
  • "Tight coupling"은 코드의 한 부분의 작은 변화가 코드의 다른 부분의 버그들의 연쇄 반응을 야기하는 의도하지 않은 결과들의 cascade로 이어질 수 있다.
  • 테스트하기 어렵도록 구현되었기 때문에 테스트 코드 작성이 어려운 것이지, 도구 숙련도의 문제인 경우가 없다.
    〰 극단적으로, 테스트 코드 작성이 쉽게 구현된 코드라면 별도의 Mock라이브러리 등의 도움 없이도 테스트 코드 작성이 쉽다.

🧵Casuses and forms of Tight Coupling (결합도가 높은 원인과 형태)

  • Coupling은 코드베이스의 서로 다른 부분들 사이의 의존성에 의해 발생한다.

  • 상위 클래스 종속성 : 하위 클래스가 부모 클래스의 구현에 크게 의존하는 경우, 부모 클래스의 변경은 모든 하위 클래스에 상당한 영향을 미칠 수 있다.

  • 여러개로 공유된 상태 : 공유되거나 변경 가능한 상태 변수 또는 개체에 의존하면 이 공유상태를 사용하는 구성 요소 간에 예측할 수 없는 결과가 나옴.

  • 구체적인 클래스 종속성 : 추상화가 아닌 구체적인 구현에 의존하면 종속 코드에 영향을 주지 않고 이러한 구현을 스왑하거나 수정하는 것이 어렵다.

  • 연쇄적인 이벤트 : 컴포넌트가 복잡한 연쇄적인 콜백이벤트로 인해 높은 결합도를 가질 때, 한 사건으로 인해 의도하지 않은 결과를 초래할 수 있다.

  • 상태 모양 종속성 : 구성 요소가 상태의 모양에 의존할 때 모양이 변경되면 끊어질 수 있다. 때문에 상태 모양이 변한다면 모든 컴포넌트의 상태를 변경하는 것 보다 선택자 함수를 변경하는 것만 해야한다.

  • 제어 종속성 구성 요서가 다른 구성 요소의 인터페이스에 의존도가 높을 경우 다른 구성 요소가 인터페이스를 변경하면 깨질 수 있다.
    네트워크 호출 또는 데이터베이스 액세스와 같은 외부 종속성을 랩핑하기 위해 facade pattern을 사용하여 어플리케이션의 나머지 부분에서 종속성의 효과를 격리시키는 것이 필요하다.

  • 시간적 결합 : 구성 요소가 연산의 순서에 의존할 때, 순서가 바뀌면 끊어질 수 있다. 순수 함수는 공유된 불변 상태에 의존하지 않고 부작용도 없기 때문에 시간적 결합을 피할 수 있는 좋은 방법이다. 동일한 입력이 주어지면, 순수 함수는 다른 함수들이 이전 또는 이후에 무엇으로 불리는지에 관계없이 항상 동일한 출력을 반환한다.

테스트 하기 좋은 코드 vs 테스트 하기 어려운 코드

테스트 하기 좋은 코드

몇번을 수행해도 항상 같은 결과가 반환되는(멱등성이 보장되는 순수함수)가 테스트하기 좋은 코드.

몇번을 수행해도 항상 같은 결과가 나오기 위해서는 아래 2가지 요소를 최대한 피해야한다.

1. 제어할 수 없는 값에 의존.

  • Random(), new Date() 와 같이 실행할 때마다 결과가 다른 함수에 의존하는 경우.
  • readLine, inputBox등 사용자들의 입력에 의존.
  • 전역함수, 전역 변수 등에 의존
  • PG사 라이브러리 등 외부 SDK에 의존.

2. 외부에 영향을 주는 코드

  • console.log, System.out.println()과 같은 표준 출력
  • Logger등을 사용.
  • 이메일 발송, 메시지큐 등 외부로의 메시지 발송
  • 데이터베이스 등에 의존 > 느린 테스트의 주범
  • 외부 API에 의존

Test First vs Test After

🌀Test First (Test Driven Development_TDD)

  • Test the Test (Watch It fail) : TDD에서는 특정 기능에 대한 테스트를 작성하고 실패하는 것을 먼저 본다. 이 실패는 테스트를 통과했을 때 테스트 된 요구사항을 제대로 구현했기 때문에 합격한 것이다.
  • Better Developer Experience : 먼저 테스트하는 것은 구현 세부사항을 조사하기도 전에 코드가 어떻게 사용될지 생각해봐야한다.
    개발자는 사용성과 명확성을 강화하면서 사용자의 관점에서 생각하도록 권장된다.
  • Clearer Requirements and Design : 테스트를 먼저 하는 것은 실행하기 전에 확고한 요구사항을 이해하는데 도움을 줄 수 있다.
  • Ensures Test Coverage : 테스트가 사후적인 생각이 아니라 개발 과정의 필수적인 부분임을 확인할 수 있다. 이는 일반적으로 더 높은 테스트 커버리지 및 더 신뢰할 수 있는 코드로 이어짐.

🌀Test After

  • Reduced Code Coverage : 예상되는 동작을 미리 정의하는게 아닌 기존 코드를 테스트하는 것에 초점을 두기 때문에 구현의 일부 측면이 간과될 위험이 있다.
    모든 요구사항이 잘 테스트되도록 보장하는 것이 아니기 때문에 테스트 적용 범위를 줄일 수 있다.
  • Possibility of Biased Tests : 코드 뒤에 테스트를 작성할 때 예상 출력을 독립적으로 계산하는 것이 아니라 코드의 현재 행동에 맞는 테스트를 작성하는 경향이 있어 실행에 대한 믿음을 너무 많이 두었기 때문에 오탐으로 이어질 수 있다.
  • Reduced Emphasis on API Design : 코드가 이미 존재하기 때문에 API를 어떻게 사용할 것인지에 따라 형형할 수 있는 기회가 줄어든다.

Seperation of Concerns

코드를 비즈니스 로직, 사용자 인터페이스, 데이터 액세스 등의 섹션으로 나누어 의도하지 않은 결과 없이 문제를 보다 효과적으로 격리하고 변경할 수 있다.
이 분리는 테스트 가능성을 도울 뿐만 아니라 소프트웨어의 유지 보수 및 확장성을 향상시킨다.

  • 비즈니스 및 상태 로직 : 애플리케이션의 핵심 로직이자 대부분의 테스트가 수행될 부분.
  • 사용자 인터페이스 : 웹 페이지 또는 모바일 앱과 같이 사용자와 상호작용 하는 응용 프로그램의 부분.
  • I/O & Effects : 데이터베이스 또는 기타 외부 데이터 소스와 상호 작용하는 응용 프로그램의 부분.

결론

👉🏻테스트 가능성은 독립형 기능이 아니라 좋은 코드 설계의 근본적인 측면이다.
Test First 접근 방식을 채택하면 개발자가 처음부터 최종 사용자 경험과 시스템 요구 사항을 고려하여 보다 강력하고 유지 관리 가능한 코드를 만들 수 있다.

코드가 기능적 요구 사항을 충족할 뿐만 아니라 탄력적이고 변화에 적응할 수 있도록 보장한다.


해당 글 원본
1. 테스트하기 좋은 코드 - 테스트하기 어려운 코드

profile
BackEnd Developer, 기록의 힘을 믿습니다.

0개의 댓글