원숭이와 함께하는 테스트 코드

redjen·2024년 6월 30일
2

월간 딥다이브

목록 보기
6/11
post-thumbnail

왜 짜야 할까, 테스트 코드

너무나도 많은 글들과 영상에서 지겹도록 테스트 코드의 필요성을 들었을 것이라 짐작한다. (때문에 이 글에서는 테스트 코드의 중요성을 이 단락 이상 말하지 않을 예정이다)

테스트 코드 작성의 장점을 극단적으로 진보시켜 취하고자 하는 개발 방법론이 TDD이고, 수 많은 테스트를 통해서 개발자가 요구 사항에 맞게 구현한 로직에 구멍은 없는지 자신할 수 있는 것이 테스트 코드의 목적, 알파이자 오메가라고 생각한다.

때문에 적어도 그 중요성을 아는 개발자는 테스트 코드를 작성하는 것이 더 이상 낯설지 않게 되었고, 수많은 아키텍쳐와 개발 방법론들의 홍수에서 그 장단점을 저울질하면서 최선의 선택을 하고 있다고 생각한다.

당연하게도 추구하는 방향과 목적성이 다르기 때문에, 몸 담고 있는 팀 또는 회사마다 코드 컨벤션이 다르듯이 테스트에 대한 컨벤션도 당연히 천차만별일 수 밖에 없다.

약 2년 정도 현업에서 일을 해보니 아무리 실력이 뛰어난 개발자라도 사람인 이상 실수를 할 수 밖에 없고 그 실수가 장애로 직결되는 등의 문제를 직접 겪을 수 있었다.

실수는 누구나 하지만, 똑같은 실수를 하지 않는 것이 중요하기 때문에 개발자는 테스트 코드를 작성한다.

무엇이 문제인가?

커져가는 비즈니스와 도메인은 코드의 복잡성을 증가시킨다. 복잡한 코드를 테스트하기는 더 복잡해진다.

코드가 복잡할 수록 테스트 코드를 작성하는 것은 매우 어려워진다는 것이 문제이다.

  • 수많은 의존성들 사이에서 알맞게 stubbing을 하는 것은 매우 어렵다.
  • 그렇게 유닛 테스트를 작성하는 일이 어려워진다면 통합 테스트를 작성하는 난이도도 자연스럽게 따라 올라가게 된다.

내가 테스트 코드를 작성하면서 겪었던 어려움을 적어보자면 아래와 같다.

1. 요구 사항의 변화

급작스럽게 특정 요구 사항이 프로덕션에 반영되어야 하는 경우가 왕왕 있다.

  • 코드 상으로는 사소한 변경인 경우 (특정 flag 값을 false에서 true로 바꾼다던지)
  • 스테이징 테스트 과정에서 발견되지 않은 버그가 리얼에서 발견되어 급하게 반영을 해야 하는 경우
  • 빈번하게 변경되는 요구 사항

무심코 지나가기 쉬운 경우이기 때문에, 테스트 코드를 이에 맞게 업데이트하는 것을 잊어버리는 경우가 있었다.

이러한 경우엔 작성한 테스트가 달성하고자 하는 목표를 제대로 달성하지 못하는 껍데기만 남은 죽은 테스트 코드가 된다.

2. 일정

마감 기한에 쫓겨 테스트 코드를 작성할 여유가 없는 경우도 왕왕 있다. 긴급하게 반영되어야 하는 경우 스테이징 환경에서 배포한 후 몇 가지 경우에 대해서 테스트를 제한적으로 진행해야 하는 경우가 속한다.

그렇다면 테스트 코드를 통해 무엇을 달성해야 할까?

  • 리얼 환경에 준하는, 최대한 다양한 경우의 input에도 구멍 없는 로직이 담긴 코드를 작성한다.
  • 아래 샘플 테스트 코드를 작성하면서 유념한 내용들이다.
    • 최대한 리소스 투자를 적게 하여 테스트를 진행하고자 했다.
    • 비즈니스 로직 검증을 컨트롤러 레이어 테스트가 아닌 서비스 레이어 테스트에서 수행했다.

문제를 어떻게 해결할 수 있을까?

최근에 사내 밋업을 통해 알게 된 테스팅 라이브러리인 픽스쳐 몽키를 소개하고자 한다.

같이 검토했었던 라이브러리로는 AutoParams가 있지만, 달성하고자 하는 목표를 여러 부분에서 픽스쳐 몽키를 통해서도 달성할 수 있다고 판단하여 오늘 논의에서는 제외하고자 한다.

무엇을 개선할 수 있을까

  • 도메인 객체들은 날이 갈수록 복잡해진다.
    • 단순히 필드 추가되는 것 뿐만 아니라, enum으로 관리되는 상태에 따라 분기가 추가되기도 한다.
  • 도메인 객체에 필드가 추가되었을 때, 추가된 필드와 관계 없는 로직의 경우 테스트가 깨지는 것을 막는다.
  • 계정과 같은 범용적인 도메인의 변화의 경우 테스트를 위해 여러 도메인 객체가 필요하고 이를 mocking 하는 것은 매우 귀찮다.
    • 계정 탈퇴 조건에 관련된 여러 도메인 객체가 관여하는 것이 예시가 될 수 있겠다. (계정 탈퇴를 위해서는 작성한 글이 하나도 존재하지 않아야 한다던지)
  • 다양한 조건을 일일이 세팅하고 이에 맞는 객체를 생성하는 일은 매우 귀찮다. (무의미하게 테스트 코드가 길어지는 경우가 많다)

테스트 픽스쳐란 무엇일까?

  • 소프트웨어 분야에서 테스트 픽스쳐는 테스트 실행을 위해 필요한 시스템의 상태와 입력 데이터를 말한다.
  • yaml 파일들을 통해 테스트에 필요한 데이터를 셋업하는 것이 예시이다.
  • 때문에 테스트 픽스쳐는 아래와 같은 장점이 있다.
    • 테스트를 반복 가능하게 한다.
    • 서로 다른 테스트 간에 필요한 메서드를 분리함으로써 테스트 코드 설계를 쉽게 해준다.
    • 이전의 테스트 수행과는 관계 없이 이번에 수행하는 테스트를 항상 초기 상태로 수행하게 해준다.

픽스쳐 몽키는 테스트 픽스쳐를 생성해주는 라이브러리이다.

1. status 필드 값에 따른 동작 분기를 테스트

db 저장 없이 생성한 테스트 픽스쳐의 값을 테스트하는 코드를 아래 요구사항에 맞추어 작성해보자.
1. 계정이 탈퇴 가능한지 체크하는 api를 작성한다고 가정하자.
2. 계정의 statusACTIVE인 경우 작성한 글 목록을 조회한다.
3. 만약 작성한 글이 하나라도 존재하는 경우 계정 탈퇴 불가능하다.
4. ACTIVE가 아닌 다른 status를 가지는 계정의 경우 아무것도 수행하지 않고 탈퇴 가능하다.

private final ArbitraryBuilder<Account> accountBuilder = FixtureMonkey.builder()
    .plugin(new JakartaValidationPlugin())
    .build()
    .giveMeBuilder(Account.class)  
    .set("accountId", Arbitraries.lazy(() -> Arbitraries.of(TEST_ACCOUNT_UIDS)))  
    .set("status", AccountStatusType.ACTIVE);

private final ArbitraryBuilder<Post> postBuilder = 

@RepeatedTest(10)
void ACTIVE_아닌_계정_상태_테스트() {
	Account testAccount = accountBuilder
		.set("status", Arbitraries.lazy(() -> Arbitraries.of(AccountStatusType.INACTIVE, AccountStatusType.LEAVE))
		.sample();

	when(accountRepository.find(any()))
		.thenReturn(Mono.just(testAccount));

	//ACTIVE 상태가 아닌 계정은 바로 탈퇴 가능함을 검증 
	StepVerifier.create(accountService.isLeavable(testAccount))
			.expectNextMatches(dto -> dto.isLeavable() == true)
			.verifyComplete();

	//PostRepository 통한 조회가 한번도 일어나지 않았음을 검증
	verify(postRepository, never()).find(any());
}

@RepeatedTest(10)
void ACTIVE_계정_상태_게시글_존재_테스트() {
	Account testAccount = accountBuilder.sample();

	when(accountRepository.find(any()))
		.thenReturn(Mono.just(testAccount));

	when(postRepository.find(any()))
		.thenReturn(Flux.fromIterable(
			FixtureMonkey.create()
			.giveMeBuilder(Post.class)
			.sampleList(3)));

	StepVerifier.create(accountService.isLeavable(testAccount))
		.expectNextMatches(dto -> dto.isLeavable() == true)
		.verifyComplete();
}

2. 게시글의 존재 유무에 따른 동작 분기를 테스트

@RepeatedTest(10)
void ACTIVE_계정_상태_게시글_미존재_테스트() {
	Account testAccount = accountBuilder.sample();

	//게시글 검색 시 빈 Flux를 반환 
	when(postRepository.find(any()))
		.thenReturn(Flux.empty());

	when(accountRepository.find(any()))
		.thenReturn(Mono.just(testAccount));

	StepVerifier.create(accountService.isLeavable(testAccount))
		.expectNextMatches(dto -> dto.isLeavable() == true)
		.verifyComplete();
}

위와 같은 방법으로 테스트 픽스쳐를 간단하게 만들어, 핵심이 되는 비즈니스 로직만을 보다 간편히 테스트할 수 있게 되었다.
상기 테스트 코드를 작성하면서 체감할 수 있었던 굵직한 장점들은 아래와 같다.

  • 테스트 대상이 되는 Account 엔티티의 필드를 전부 채울 필요 없이, 랜덤하게 생성되는 값들을 기반하되 로직에 관여하는 필드만 임의로 세팅이 가능했다.
  • Not 조건을 테스트하기 위해 복수개의 enum 값에서 랜덤하게 선택한 값에 대해서 하나의 테스트 코드로 함께 테스트 가능했다. 즉 INACTIVE, LEAVE 상태를 굳이 구분지어 테스트 하기 위한 불필요한 테스트 코드가 줄어들었다.
  • 핵심 검증 대상이 아닌 Post 테스트 픽스쳐를 생성하기 위해 고민을 많이 하지 않을 수 있었다. 랜덤하게 채워지는 값들은 constraint 제약 조건을 따라서 생성되기 때문이다.

FixtureMonkey와 여러 plugin들

타 라이브러리와 같이 각자 프로젝트 상황과 코드 컨벤션에 따라 테스트 픽스쳐를 제한적으로 만들어야 하는 상황들이 다양할 것이다.

픽스쳐 몽키 라이브러리는 이를 해결하기 위해 여러 plugin들을 제공해준다.

Kotlin Plugin

사내 밋업에서 볼 수 있었던 다양한 예시는 코틀린 기반의 코드들이 많았다. 프로젝트를 kotlin을 사용해 개발한다면 테스트 픽스쳐 생성 부분에 있어 상당히 매끄럽게 진행할 수 있는 것 같았다.
(kotest plugin도 따로 존재한다..! 하지만 이부분은 아직 잘 모르기 때문에..)

Jackson Plugin

spring의 기본 serializer / deserializer인 jackson 라이브러리와도 궁합이 좋다.

FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
    .plugin(new JacksonPlugin())
    .build();

위와 같은 방식으로 플러그인을 적용한다면, 컨트롤러 레벨에서 @JsonIgnore, @JsonProperty와 같은 어노테이션의 동작또한 테스트할 수 있다.

Jakarta Validation Plugin

위에서 언급했던 Jakarta Validation / Hibernate validator 플러그인이다. 테스트 픽스쳐의 필드 설정 시 validation을 만족하는 값만 랜덤하게 생성되도록 돕는다.

@Min 과 같은 어노테이션을 테스트하기 좋다.

마치며

항상 테스트 코드를 어떻게 잘 효율적으로 작성할 수 있을지에 대한 고민이 있는 개발자라면 적극적으로 활용해보면 분명 장점이 있을 것이라 느꼈다.

체감했던 장점을 다시 한번 요약하자면

  • 테스트 픽스쳐에 랜덤성을 더해 테스트 케이스 작성 당시 미처 캐치하지 못했던 엣지 케이스를 잡을 수 있다.
  • 빌더 재사용 및 설정 값의 랜덤 풀 제어를 통해 불필요한 테스트 코드를 줄일 수 있다.
  • 몇 가지 플러그인들을 통해 각자 환경에서 적용하기 쉽다
profile
make maketh install

0개의 댓글

관련 채용 정보