코딩은 내일부터

동글 프로젝트 테스트 코드 개선 및 최적화 본문

우아한 테크 코스(우테코)/우테코 공부

동글 프로젝트 테스트 코드 개선 및 최적화

zl존 비버 2023. 10. 1. 17:43
728x90

전쟁을 시작하기 전

처음 동글 프로젝트의 테스트코드의 개수는 104개, 커버리지는 50%였다.

기능구현 하는데 집중해서 테스트코드가 빈약한 상태이다.(사실 핑계 아닌 핑계..)

 

 

 

100%를 어떻게 하셨...

그래서 이번 추석연휴가 길어 테스트코드 커버리지를 올릴 생각이다.

위에 이응준님 처럼 커버리지 100%는 아니지만 80%까지를 목표를 시작했다.

그리고 동글의 자신감을 되찾아줄 예정이다!

 

 

 

 

어떻게 테스트를 작성할 것인가?

레이어를 독립적으로 테스트하기 위해 슬라이스 테스트 방식으로 진행했다.

쉽게 말해 controller, service, repository 계층 각각을 단위테스트 한다고 생각하면 된다.

 

 

Repository Layer Test

먼저 Reositroy 테스트는 @DataJpaTest를 사용해서

Repositroy 테스트에 필요하지 않은 @Controller, @Service를 빈으로 등록하지 않도록 하여 슬라이스 테스트를 진행했다.

 

 

 

 

 

Service Layer Test

고민

Service Layer Test를 할 때 Mock을 사용해서 테스트를 할까

@SpringBootTest를 사용해서 실제 객체를 가지고 테스트를 해야 하나 고민했다.

결론

내가 내린 결론은 도메인과 Repository, 그리고 인수 테스트를 통해 로직을 검증받고,

service layer는 행위를 검증하는 Mockito를 사용해서

계층(Repository 계층) 간의 의존을 하지 않아 테스트하려는 부분에 집중하고,

테스트의 속도를 높여 빠른 피드백을 받을 수 있어 Mockito를 사용하자고 결론을 내렸다.

문제 발생

service test는 위에 사진과 같이 @Mock을 사용해서 테스트를 진행할 수 있었다.

원래 있던 service test를 mock으로 변경을 하고,

테스트가 안된 부분을 mock으로 테스트하던 도중 mock을 사용해서 테스트가 어려운 부분이 생겼다.

 

할 수는 있었지만, 복잡한 로직에서는 너무 많은 stubing을 사용해서 오히려 가독성이 떨어지는 거였다.

(물론 stubing을 너무 많이 한다는 게 하나의 메서드가 너무 많은 일을 하고 있다는 뜻이니 리펙터링을 해야 한다.)

 

mock과 @SpringBootTest를 혼합해서 사용해야 하는데 내가 원하는 처리속도를 챙길 수 있을지 의문이었다.

 

찾아본 결과 mock은 프록시 (Proxy)와 리플렉션 (Reflection)을 사용해서 처리속도가 빨라 성능적으로 유리하다는 것이다.

 

즉, mock은 실제 객체를 생성하지 않고, 메서드 호출이 발생하면

미리 정의된 동작 (예를 들어, 특정 값을 반환)을 수행하기 때문에 처리속도가 빠르기 때문에

대부분을 테스트는 mock을 사용하고 일부분만 @SpringBootTest를 사용해서 service test를 작성했다.

 

 

 

Controller Layer Test

컨트롤러 테스트는 @WebMvcTest를 사용해서 슬라이스테스트는 진행했고

@WebMvcTest를 사용해서  @Service를 빈으로 등록되지 않았기 때문에, 테스트를 진행할 수 없다.

그래서 위의 사진과 같이 @MockBean을 이용했다.

Mock Bean은 mock과 비슷하게 기존 Bean의 껍데기만 가져오고 내부 구현은 사용자에게 위임한 형태이다.

어떤 로직에 대해 Bean이 예상대로 동작하도록 하고 싶을 때, Mock Bean을 사용하는 것이다.

 

 

테스트 커버리지 80% 달성

추석을 갈아 넣어 드디어 테스트 커버리지 80%를 달성했다.

총 테스트 160개를 만들었는데 원래 작성한 테스트를 변경하는 작업에서 시간이 많이 걸린 거 같다.

이렇게 테스트를 많이 만들고 전체 테스트를 실행시키면 몇 초가 걸리는지 측정해 봤다.

대략 20 초가 걸리고 Application Context는 16번 로드하고 있었다.

  • 인수테스트(@SpringBootTest)(작성 중)
  • 컨트롤러 계층 테스트(@WebMvcTest)
  • 서비스 계층 테스트(@SpringBootTest or Mockito)
  • Repository 계층 테스트(@DataJpaTest)

시간이 느려지게 Context를 여러 번 로드하는 게 원인이라 생각해서

위와 같이 계층마다 테스트를 진행하고 있는데 4종류의 테스트마다 하나의 Context을 띄우는 것을 목표로 했다.

 

 

Intellij가 중간에 새롭게 실행되는 Application Context Load시간을 테스트 시간에 누적하지 않아서

동영상으로 실제 테스트 시간을 측정했습니다.

 

@DirtiesContext 제거하기

@DirtiesContext 제거 전

테스트가 서로 간섭을 하고 있어 @DirtiesContext를 사용해서 테스트 간의 격리를 해준 상태다.

하지만 위 사진에서 사용한 @DirtiesContext는 @Test 어노테이션마다 Context를 재생성하기 때문에 시간이 오래 걸린다.

그래서 데이터 격리를 mock을 사용해서 해결하고,

@DirtiesContext를 제거해서 메서드마다 Context를 재생성을 줄였다.

@DirtiesContext 제거 후

 

 

 

 

Context Caching

SpringBoot에서는 위와 같이 캐싱을 할 수 있는데 @MockBean(@SpyBean 도!)을 테스트마다 다르게 사용하면

테스트가 오염됐다 생각해서 새로운 Context를 띄운다.

 

Controller Layer에서 테스트를 진행할 때 사용한 @MockBean으로 인해 Context가 여러 번 띄워지는 것이었다….

그래서 필요한 @MockBean 객체를 한 번에 선언하여 Context에 올려두고,

Controller Layer Test Class에 상속을 받아 Controller Test를 진행할 때는 1번의 Context만 띄우도록 아래와 같이 수정했다.

 

 

 

성능 측정

이렇게 테스트를 바꾸고 성능 측정을 해봤다.

시간은 9초, Context는 총 3번 띄운다.(아직 인수테스트가 없어서 3번이다.)

 

 

결론

결론적으로 55%의 성능향상을 하여 보다 빠른 피드백과 배포를 할 수 있게 됐고,

테스트 커버리지가 높아도 버그는 발생하지만,

이전처럼 배포를 하고 버그가 일어날까 봐 불안한 마음을 조금 덜 수 있다.!

눙물,,,,

 

적용한 코드

https://github.com/woowacourse-teams/2023-dong-gle/pull/501