본문 바로가기
개발자 준비/TDD

어떻게 하면 테스트 주도 개발을 잘할까?

by osul_world 2022. 11. 8.
728x90

목차


  • 왜 테스트 주도 개발을 해야할까?
  • 테스트 주도 개발 순서
  • 기능 목록은 어떻게 작성해야할까?
  • 테스트가 어려운 설계 특징 및 대처법
  • 테스트 코드 골격
  • 테스트 종류

 

왜 테스트 주도 개발을 해야 하는가?


테스트 주도 개발은 내 코드의 품질과 유연함을 적은 비용으로 향상시킬 수 있는 강력한 도구 입니다

  • 테스트가 가능한 설계 구조에는 SOLID, 클린코드, 방어코드 등이 포함되기 때문에 코드 품질을 자연스럽게 확보할 수 있습니다
  • TDD를 하는 과정에서 작성한 테스트 코드는 CI/CD에서 자동화 테스트로 사용되어 버그가 배포되는 것을 막아주고 이는 소프트웨어 품질이 저하되는것을 방지합니다
  • 구체적인 예를 통해 모호함을 없애고 정확한 코드를 추적할 수 있게 해주며, 코드에 대한 빠른 피드백으로 안전한 리펙토링을 보장할 수 있습니다

 

 

테스트 주도 개발 순서


구현하기 쉬운 테스트부터 먼저 작성하고 테스트에 실패하면 테스트를 통과시킬 만큼 코드를 추가하는 과정을 반복하면서 점진적으로 기능을 완성해 나가면 됩니다

 

  1. 기능을 검증하는 테스트 코드를 먼저 작성한다
  2. 테스트를 통과하기 위한 기능을 구현한다
  3. 테스트를 실행했고 테스트를 통과 시킨다
  4. 리펙토링 한다 (이때 테스트 코드로 리펙토링 과정을 검토한다 )
  5. 기능이나 예외사항을 다음 대상으로 삼는다
  6. (반복)

 

 

테스트 주도 개발에서 기능 목록


개발자는 모호함을 없애고 정확한 코드를 추적할 수 있어야 합니다, 이를 위해 예외적인 상황이나 복잡한 상황에 해당하는 구체적인 예를 끄집어내고 점진적으로 기능 명세를 구체화할 필요가 있습니다

  • 단, 메서드 명, 입/출력은 언제든지 변경될 수 있기에 처음 부터 구체화하려고 하지말고 구현할 기능과 예외사항을 위주로 명세하고 점차 업데이트해 나아가면 됩니다

 

 

테스트가 어려운 설계 특징 및 대처법


모든 코드를 테스트 할 수 있으면 좋겠지만, 개발을 하다보면 테스트가 어려운 코드를 만나게 됩니다

외부와의 의존관계, 뒤엉킨 역할과 책임 , 접근자 등 테스트가 어려운 코드들의 특징을 정리하고 어떻게 대처해야할지 소개하겠습니다

 

 

외부에 의존하는 경우

public class PaySync {
    private PayInfoDao payInfoDao = new PayInfoDao();
		...
}

이 코드를 테스트하려면 PayInfoDao가 올바르게 동작하는데 필요한 모든 환경이 구성되어야 합니다

또한, 데이터 베이스와 의존한다면 테스트 내용이 데이터 베이스에 영향을 주기 때문에 롤백 작업이 추가 됩니다

 

이런 경우, 개발자가 동작을 직접 제어할 수 있는 가짜(Mock) 객체 및 서버를 지원하는 테스트 프레임워크를 사용할 수 있습니다

  • 대표적으로 Mockito가 있습니다
  • Mock에 대한 자세한 내용은 여기에서 확인 하실 수 있습니다

 

역할과 책임이 복잡한 경우

여러 역할이 섞여 있는 코드는 특정 기능만 테스트하기가 쉽지 않습니다, 특정 기능만 테스트하기 위해서는 해당 기능과 의존관계에 있는 모든 상태를 설정해야합니다

 

이런 경우, 리펙토링을 진행하여 코드 가독성을 올리거나, SRP(단일 책임 원칙)을 준수하는지 다시한번 확인해 클래스나 메서드를 분리할 필요가 있습니다

 

또한, DI를 관리하는 클래스를 생성해 의존관계를 한번에 관리하도록 개선할 필요가 있습니다

 

 

객체지향의 은닉을 깨야하는 경우

특정 기능을 테스트하려면 상태값 확인을 위해 getter, setter가 필요하거나, private를 public하게 변경해야하는 경우가 있습니다

 

하지만 객체지향이 숨긴 코드들을 테스트를 위해 개방하거나 메서드가 추가하게 되면, 은닉성을 해치게 되고 단일 책임 원칙을 위배할 수 있습니다

  • 때문에, 테스트를 위해 데이터를 직접 가져오거나, 접근자를 변경하는 것은 피하는게 좋습니다

 

이를 해결하기 위해, 다음과 같은 방법을 고려할 수 있습니다

  • 리플랙션을 이용해 테스트시 일시적으로 접근자를 변경
  • 데이터를 직접 조회하지 않고 객체에 메세지를 보내 보낸 데이터가 맞는지 확인하는 메서드를 추가

 

 

테스트 코드 골격


어떤 상황이 주어지고, 그 상황에서 기능을 실행하고, 실행한 결과를 확인하는 세 가지가 테스트 코드의 기본 골격을 이룹니다

// 상황, 실행, 결과 확인은 영어 표현 given, when, then에 대응
//given  : 이런 상황에서 

//when : 무엇을 실행하면

//then : 이런 결과가 나와야한다

 

 

테스트 종류별 특징


기능 테스트와 E2E 테스트

기능 테스트(Functional Testing)는 사용자 입장에서 시스템이 제공하는 기능이 올바르게 동작하는지 확인한다. 예를 들어 회원 가입 기능이 올바르게 작동하는지 확인하려면 웹 서버, 데이터베이스, 웹 브라우저가 필요하다.

기능 테스트는 끝에서 끝까지 올바른지 검사하기 때문에 E2E(End to end) 테스트로도 볼 수 있다. QA 조직에서 수행하는 테스트가 주로 기능 테스트이다. 이때 테스트는 시스템이 필요로 하는 데이터를 입력하고 결과가 올바른지 확인한다.

 

통합 테스트

통합 테스트(Integration Testing)는 시스템의 각 구성 요소가 올바르게 연동되는지 확인한다. 기능 테스트가 사용자 입장에서 테스트하는 데 반해 통합 테스트는 소프트웨어의 코드를 직접 테스트한다. 모바일 앱을 예로 들면 기능 테스트는 앱을 통해 가입 기능을 테스트한다면 통합 테스트는 서버의 회원 가입 코드를 직접 테스트하는 식이다. 일반적인 웹 애플리케이션은 프레임워크, 라이브러리, 데이터베이스, 구현한 코드가 주요 통합 테스트 대상이다.

 

단위 테스트

단위 테스트는 개별 코드나 컴포넌트가 기대한대로 동작하는지 확인한다. 단위 테스트는 한 클래스나 한 메서드와 같은 작은 범위를 테스트한다. 일부 의존 대상은 스텁이나 모의 객체 등을 이용해서 대역으로 대체한다.

 

 

 

테스트 코드 유지보수

  • 단위 테스트 코드를 작성하다 보면 상황 구성을 위해 필요한 데이터가 다소 복잡할 때가 있다. Mock을 이용하자
  • getMoney()같은 기댓값을 사용하기 보다, 상수로 정확히 표현하기
  • 조건문 쓰지 않기
  • 한번에 하나만 테스트하기
  • 테스트 코드 방치하지 않기

 

 

Reference

https://ko.wikipedia.org/wiki/모의_객체

https://incheol-jung.gitbook.io/docs/study/undefined-3

https://velog.io/@seongwon97/Unit-Test-단위-테스트

https://seongwon.dev/Java/20211127-Unit-Test란/

https://yohanpro.com/posts/programming/tdd

https://softwareengineering.stackexchange.com/questions/135047/should-i-avoid-private-methods-if-i-perform-tdd

https://justhackem.wordpress.com/2017/09/29/should-private-methods-be-tested/

728x90