본문 바로가기
프로젝트/협업 프로젝트(2023.12.18-2024.01.25)

[Key Word 개발기] @WebMvcTest 없이 테스트??

by dal_been 2024. 1. 6.
728x90

부트캠프 협업프로젝트하면서 멘토님께 일주일에 한번씩 멘토링을 받는데 내 테스트 코드에 대한 멘토를 해주셨다.

해주시면서 하신 말씀이 이해가 안되어 찾아보니 내가 정말 아무생각없이 테스트코드 공장마냥 짜내고 있었구나...생각이 되었다.

 


@WebMvcTest 없이 테스트 해봐유??

 

멘토님께서

mocking테스트를 추천하지만 @SpringBootTest, @DataJpaTest, @WebMvcTest의 경우 어플리케이션을 실제로
띄운후 테스트하는 방식이기때문에 단순히 mocking만으로 테스트하는게 성능,시간 문제를 해결하고 테스트하고자하는
목적들을 달성할 수 있다.

 

라고 말씀하셨다.

 

엥?? 저 어노테이션 없이 어떻게 하라는 거죠..?? 테스트코드에는 저런 어노테이션이 필수 인거아닌가??라는 생각이 머리속으로 스쳐지나갔다.

 

아 일단 모르겠고 저 세개 어노테이션을 파보자.

 

 

@SpringBootTest, @DataJpaTest, @WebMvcTest

 

간단하게 내가 알고 있는 개념부터 정리하자면.

 

@SpringBootTest의 경우 통합테스트로서 기능검증을 위한 것이 아니라 전체적으로 플로우가 제대로 동작하는지 검증하기 위해 사용한다. 그렇기때문에 애플맄이션의 설정, 모든 Bean을 로드하여 운영환경과 가장 유사한 테스트가 가능하다.

다만 애플리케이션 설정, 모든 Bean을 로드하기 때문에 시간이 오래걸리고 무겁다.

 

@WebMvcTest의 경우 단위 테스트지만 애플리케이션 컨텍스트를 구성한다(스프링에서 IoC (Inversion of Control) 컨테이너로, 애플리케이션의 객체들을 생성하고 관리하는 역할) 물론 @SpringBootTest처럼 모든 Bean을 로드하는 것은 아니고

@MockBean, @SpyBean
@TestPropertySource
@ConditionalOnX
@WebMvcTest에 컨트롤러 지정
@Import

 

과 같은 bean들이 주입된다.

 

@DataJpaTest의 경우 애플리케이션 컨텍스트에서 JPA에 필요한 설정들만 등록한다. 예를들어 @Entity, Spring Data Jpa Repository타입의 빈들을 자동 구성한다.

 

내가 알고 있는 것들을 정리해보니 결국 셋다 Application Context를 주입받네..???

 

 

조금만 더 자세하게 코드를 보자면

@SpringBootTest, @DataJpaTest, @WebMvcTest을 파고 들어가보면

@ExtendWith(SpringExtension.class)

 

이게 공통적으로 붙여져있다. 이것이 무엇인데..?

 

 

SpringExtension??

 

@ExtendWith는 단위테스트를 가능하게 해준다.

근데 여기서 MockitoExtension과 SpringExtension이 들어갈 수 있는데 둘의 차이는 뭘까??

 

SpringExtension을 파보면 알 수 있다.

 

결과적으로 말하자면 SpringExtension에서 ApplicationContext를 만든다.

여기서 중심적으로 봐야하는 것은 TextContext와 TextExecutionListener이다.

 

PostProcessTestInstance 메서드를 보면

@Override
public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
	validateAutowiredConfig(context);
	getTestContextManager(context).prepareTestInstance(testInstance);
 }

 

이 메서드를 통해 테스트 인스턴스가 생성되고 난후의 할일을 정의한다.

여기서 prepareTestInstance를 조금더 파고 들어가보면

 

public void prepareTestInstance(Object testInstance) throws Exception {
	if (logger.isTraceEnabled()) {
		logger.trace("prepareTestInstance(): instance [" + testInstance + "]");
	}
	getTestContext().updateState(testInstance, null, null);

	for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
		try {
			testExecutionListener.prepareTestInstance(getTestContext());
		}
		catch (Throwable ex) {
			if (logger.isErrorEnabled()) {
				logger.error("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
						"] to prepare test instance [" + testInstance + "]", ex);
			}
			ReflectionUtils.rethrowException(ex);
		}
	}
}

 

testExcutionListener.prepareTestInstance(getTestContext())에서 ApplicationContext를 준비한다.

 

그럼 TestExecutionListener를 상속한 클래스 중 하나를 파보면(DependencyInjectionTestExecutionListener)

 

@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
	if (logger.isDebugEnabled()) {
		logger.debug("Performing dependency injection for test context [" + testContext + "].");
	}
	injectDependencies(testContext);
}


protected void injectDependencies(TestContext testContext) throws Exception {
	Object bean = testContext.getTestInstance();
	Class<?> clazz = testContext.getTestClass();
	AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();
	beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
	beanFactory.initializeBean(bean, clazz.getName() + AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX);
	testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
}

 

prepareTestInstance -> injectDependecies 안에 testContext.getApplicationContext()를 하는 것을 볼 수 있을 것이다.

즉 여기서 ApplicationContext를 생성한다.

 

 

조금더 SpringDocs를 통해 파고들자면

 

TestConTextManger

TestContextManager is the main entry point into the Spring TestContext Framework,
which provides support for loading and accessing application contexts,
dependency injection of test instances, transactional execution of test methods, etc.

Specifically, a TestContextManager is responsible for managing a single TestContext 
and signaling events to all registered TestExecutionListeners at well defined test execution points:

 

TestContextManger를 통해서 Application 제공하고, 의존성, 트랜젝션을 관리한다.

구체적으로는 TestContext를 관리하고 TestExecutionListeners에게 정의된 테스트 실행 지점에서 이벤트 신호를 전달하는 역할을 한다. 

이런 느낌이다.

 

정리하자면 어째든 @SpringBootTest, @DataJpaTest, @WebMvcTest은 SpringExtension 어노테이션을 통해 ApplicationContext를 띄우는 구나..!

 

사실 어떤 분의 깃코드를 보다가 @WebMvcTest를 하실때 하나의 class로 만들어서 필요한 모든 controller를 로드한후에 controller test클래스가 저 클래스를 상속하여 만든 테스트코드를 보았다. 맨처음에는 굳이..?라는 생각이 들었지만. 효율적? 인 방법인 것같다.

어처피 controller테스트에서 @WebMvcTest를 이용할 것이라면 각 controller 클래스마다 ApplicationContext를 로드하는 것보다 한번에 ApplicaionContext를 로드하는게 조금더 나은 방향이라고 생각한다.

 

 

MockitoExtension

 

SpringDocs를 찾아보려고 했는데 내가 못찾아서... 일단 검색 해보니 좋은 답변이 있었다.

 

When NOT involving Spring:

If you just want to involve Mockito and don't have to involve Spring, 
for example, when you just want to use the @Mock / @InjectMocks annotations, 
then you want to use @ExtendWith(MockitoExtension.class), 
as it doesn't load in a bunch of unneeded Spring stuff. 
It replaces the deprecated JUnit4 @RunWith(MockitoJUnitRunner.class).

 

그니까 MockitoExtension은 SpringExtension과 다르게 Spring관련 설정들을 로드하지 않는다. 즉 Spring test framework와 관련된 설정들을 로드할 필요가 없다면 MockitoExtesion을 사용하는 것이다.

 

 


음음..멘토님의 말씀이 조금이해가 된다. 어처피 단위테스트를 통해 내가 작성한 코드가 내가 의도한바에 따라 잘 작동하는지 확인하는 것이라면 굳이 ApplicationContext를 띄울 필요가 있을까?? 일단 애플리케이션 컨텍스트를 띄우는 순간 어째든 필요한 Bean, 설정을 로드하기 때문에 느려질 수 밖에 없다. 물론 우리 프로젝트가 작아서 느려질거같지느 않지만...ㅎㅎㅎ

 

 

 

https://mangkyu.tistory.com/244

https://6161990src.tistory.com/126

https://whatasame.tistory.com/2