테스트 코드 작성 출발선에서 봐야할 글

텐저린티·2023년 6월 30일
0

알쓸신잡

목록 보기
1/10
post-thumbnail

바우처 과제를 하면서 테스트 때문에 눈물나는 일이 많다.

동료들도 그렇고 멘토님도 그렇고 테스트에 대해서 많은 관심을 가지고 시행착오를 겪는 모양이다.

동료 중에 테스트 코드 작성하기 전에 보면 좋을 것 같다고 공유한 블로그가 있다.

잘 정리해서 알차게 써먹고 싶어서 요약정리하려고 한다.

좋은 글 많으니 자주 가보면 좋을 것 같다.

향로 블로그 - 테스트하기 좋은 코드 - 테스트하기 어려운 코드
정진욱 블로그 - Testing, Oh my!


테스트하기 쉬운코드

테스트 코드 작성 어려운 코드 vs 쉬운 코드

테스트 코드 작성 어려운 구현체 때문에 잘 안되는 것

  • 테스트 코드 작성이 쉽게 구현된 코드면 별도 테스트 라이브러리 도움 없이 충분히 테스트 가능

테스트코드 작성 위해 원본 코드 바꿔도 된다.

  • 테스트는 보조수단 X
  • 구현 설계 smell을 맡게해주는 수단
  • 좋은 디자인으로 구현된 코드는 대부분 테스트가 쉬움
    • 따라서 테스트가 어렵다면 구현을 바꾸는 방법도 좋은 방법임

의도적으로 @SpyBean, @MockBean을 쓰지 않도록 하는 것도 좋은 방법

테스트하기 좋은 코드

  • 항상 같은 결과가 반환되는 함수 (멱등성 보장 함수)

테스트하기 나쁜 코드는

  • 제어할 수 없는 값에 의존하는 경우 → 언제나 동일한 결과를 보장하지 못하기 때문
    • 전역함수, 전역변수
    • 실행마다 결과가 다른 함수
      • 랜덤값, 날짜
    • 외부 라이브러리/SDK 의존
    • 사용자 입력
  • 외부에 영향을 주는 코드
    • 표준 출력
    • logger
    • 이메일, 메시지큐 등 외부로 메시지 발송
    • 데이터베이스 의존
    • 외부 API 의존

제어할 수 없는 값에 의존하는 경우 개선

문제상황

  • 현재 시간에 영향을 받는 Order 도메인
  • 실행할 때마다 항상 결과 다름
  • 덕분에 도메인 로직이 테스트하기 어려움
  • 문제는 도메인이므로 계층 전반의 테스트가 함께 어려워짐
    • 도메인 로직에 의존하는 모든 계층 (Controller, Service, Repository, Domain)이 모두 테스트가 어려워짐
class Order {
	private long amount;
	
	void discount() {
		var now = LocalDateTime.now();
		if (now.dayOfWeek() == DayOfWeek.SUNDAY) {
			this.amount = amount * 0.9;
		}
	}
}

해결방법

생성자, 메소드 인자로 제어할 수 있는 값을 외부에서 주입하는 방법

// domain logic
void discountWith(LocalDateTime now) {
	if (now.dayOfWeek() == DayOfWeek.SUNDAY) {
		this.amount = amount*0.9;
	}
}

// using domain logic
var sut = new Order(100);
var now = LocalDateTime.of(2023,8,14,10,15,0);
sut.discountWith(now);
  • 함수 인자가 길어지는 경향이 있음
    • Dto 패턴으로 묶어서 전달하는 방법으로 해결
    • 함수 인자 기본값을 사용하는 방법으로 해결

가장 코드 의존이 적은 영역까지 제어할 수 없는 값을 밀어내기

  • 최종적으로 Contoller까지 밀어내는 것
  • Controller를 제외한 나머지 계층은 테스트하기 어려운 코드와 분리됨
  • 요약하면, 프로그램 진입점이 되는 영역에 테스트하기 어려운 코드를 모아두고, 나머지 계층은 테스트하기 좋은 코드로만 구성하는 것

의존성 주입

  • 제어할 수 없는 값을 반환하는 인터페이스를 두고, 테스트와 메인코드에서 활용하는 방법
  • 구현체는 생성자를 통해 제어할 수 있는 값을 받아 사용할 수 있음
// 제어할 수 없는 값 반환 인터페이스
interface Time {
	LocalDateTime now();
}

// 메인코드에서 활용
class SeoulTime implements Time {
	@Override
	public LocalDateTime now() {
		return LocalDateTime.now();
	}
}

// 테스트코드에서 활용
class StubTime implements Time {
	private LocalDateTime currentTime;
	
	public StubTime(Number year, Number month, Number day, Number hour, ...) {
		currentTime = LocalDateTime.of(....);
	}

	@Override
	public LocalDateTime now() {
		return this.currentTime;
	}
}

외부에 의존하는 코드 개선

문제상황

  • Active Record 패턴 → 이 방법 지양할 것!
  • 기능 3가지
    • 검증 로직
    • 취소 주문 생성
    • DB 취소 주문 저장
  • 문제점
    • 복잡한 테스트 환경 구축
      • 로직이 많기 때문
      • 테스트 환경 구축에 많은 리소스 필요
    • 낮은 테스트 리팩토링 내구성
      • 외부 의존 대상 교체마다 변화 요구가 커짐
    • 지키기 어려운 일관성
      • 언제 실행해도 같은 결과를 얻기 어려워짐
    • 느린 테스트
      • 외부 의존성이 있는 테스트는 수행속도 느림

해결 방법

  • 외부 의존성을 로직에서 떨어뜨려 놓는게 좋다
    • DB 로직 밖으로 빼고, 검증 로직 밖으로 빼고 이런식 ㅇㅇ
  • 외부 스토리지 사용하는 코드와 도메인을 분리해야 한다.
profile
개발하고 말테야

0개의 댓글

관련 채용 정보