Spock을 이용하여 MockBean을 생성하였는데, 정의한 행위대로 동작하지 않는 이슈를 발견했다.
간단히 작성해보자면 아래와 비스무리한 코드였다.
@Autowired
private SimpleMapper simpleMapper;
...
@TestConfiguration
def setup() {
def factory = new DetachedMockFactory()
@Bean
SimpleMapper simpleMapper() {
return factory.Mock(SimpleMapper)
}
}
def "test"() {
given:
simpleService.selectSomething(_) >> "SUCCESS"
expect:
"SUCCESS" == simpleService.selectSomething("Hello") // reutrn null
}
MockBean이 아닌 Mock객체를 생성하였을 때는 정의한 행위대로 동작을 잘했지만, Mock객체를 Bean으로 등록하는 순간 내가 원하는대로 동작하지 않았다.
이것 때문에 Spock, Spring 버전 확인부터 시작하여 엄청난 삽질을 하였는데 원인을 알았을 때는 뭔가 허무허무
원인
원인부터 말하자면 AOP
설정 때문이였다.
디버깅하면서 살펴보니 정상동작하는 Mock 객체와 정상동작하지 않는 Mock 객체의 차이점이 보였다.
- 정상동작 x -
JdkDynamicAopProxy
로 생성됨 - 정상동작 o -
DynamicProxyMockInterceptrAdapter
로 생성됨
AOP 설정이 된 경우, Spock이 만든 Proxy객체를 Spring이 한번 더 감싼 것이다.
Spock 에서 factory.Mock(SimpleMapper)
시점에서 Proxy객체를 한번 생성하고, 스프링이 Spock이 만든 Proxy객체를 다시한번 Aop Proxy로 생성한다.
SimpleMapper --Spock--> Proxy(InvocationHandler : DynamicProxyMockInterceptorAdapter) --Spring--> Proxy(InvocationHandler : JdkDynamicAopProxy)
따라서, Spock에서 정의한 메소드를 실행하게되면 타입 불일치(DynamicProxyMockInterceptorAdapter != JdkDynamicAopProxy) 로 인하여 최종적으로 null을 반환하게 되는 것이다.
MockObject.java
@Override
public boolean matches(Object target, IMockInteraction interaction) {
if (target instanceof Wildcard) return verified || !interaction.isRequired();
boolean match = global ? matchGlobal(target) : instance == target;
if (match) {
checkRequiredInteractionAllowed(interaction);
}
return match;
}
private boolean matchGlobal(Object target) {
return this.instance.getClass() == target.getClass() && (!this.isMockOfClass() || this.instance == target);
}
여기서 instance는 DynamicProxyMockInterceptorAdapter, target은 JdkDynamicAopProxy가 된다.
관련 이슈) https://github.com/spockframework/spock/issues/758
문제해결
spock 1.1
해당 문제를 해결하려면 JdkDynamicProxy로 생성된 경우 Porxy의 Target을 반환하는 작업이 필요
((Advised) jdkDynamicProxy).getTargetSource().getTarget();
but, 매퍼/서비스 하나의 객체 테스트면 상관이 없지만, 해당 객체에 의존성을 갖고 있는 여러 객체들을 테스트하기에는 힘들어 보인다.(그래서 버전업을 함)
spock 1.2
spock1.2에서는 해당 이슈를 해결할 수 있는 여러 어노테이션들이 생겼다.
1. @UnwrapAopProxy
기존 코드에 @UnwrapAopProxy를 설정하면 런타임시에 Spock이 AopProxy 인지 체크하여, Target클래스를 반환하는 작업을 해준다.(proxy를 unwrap)
@UnwrapAopProxy
@Autowired
SimpleCardMapper simpleCardMapper
2. @SpringBean
테스트 컨텍스트에 빈으로 mock/stub/spy 를 등록할 수 있게 해준다.
ApplicationContext에 이미 존재하는 빈들을 교체해줌(SpockMockPostProcessor.java)
TestConfiguration가 없어도 field 선언시 어노테이션을 사용하면 쉽게 Mock/Spy Bean객체를 생성할 수 있게 된다.
@SpringBean
SimpleCardMapper simpleCardMapper = Mock()
Proxy짤막 지식
더 자세한 내용은 구글링으로!!
Proxy?
- 타깃과 같은 메서드를 구현하고 있다가 메서드가 호출되면 타깃 오브젝트로 위임해주는 것(부가기능 수행 가능)
JDK Dynamic Proxy
- 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트
- 프록시 팩토리에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스 오브젝트를 자동으로 생성
- 인터페이스가 반드시 존재해야함. 부가기능은 직접 작성(InvocationHandler)
- Proxy.newProxyInstance()로 생성함(나중에 포스팅해보자)
CGLIB Proxy
- 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트
- 클래스 상속을 이용하여 생성하기 때문에 인터페이스가 존재하지 않아도 가능
- 단 부가기능은 직접 작성(MethodInterceptor)
'프로그래밍 노트 > 트러블슈팅' 카테고리의 다른 글
[Kafka] 카프카 중복 메시지 발생 원인 삽질노트 (0) | 2023.04.04 |
---|---|
SpringBoot, Spock test 미실행 이슈(feat. gradle) (0) | 2022.08.11 |
[Mybatis] 동적 쿼리 DBMS별 Like 문 (0) | 2019.07.12 |
[Maven] error resolving version for plugin from the repositories ... 오류 (0) | 2019.03.26 |
AES256 암호화시 java.security.InvalidKeyException: Illegal key size 해결방안 (3) | 2019.03.06 |