1.问题描述:
使用ReflectionTestUtils.setField注入mock对象,对于使用代理的类,不能够将mock对象注入进去。
ationContext-test.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns="http://www.springframework.org/schema/beans" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--扫描的包-->
<context:component-scan base-package="adapter"/>
<context:component-scan base-package="service"/>
<tx:annotation-driven />
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--UT采用H2内存数据库来消除UT对环境的依赖-->
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:db/schema.sql" />
</jdbc:embedded-database>
<!--生成代码使用CGLIB代理-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
Service类
public interface AService {
String testA(Integer num);
}
@Service
public class AServiceImpl implements AService {
@Autowired
private CAdapter cAdapter;
@Override
public String testA(Integer num) {
return cAdapter.testAdapter() + num;
}
}
public interface BService {
String testB(Integer num);
String testB2(Integer num);
}
@Service
public class BServiceImpl implements BService {
@Autowired
private CAdapter cAdapter;
@Override
public String testB(Integer num) {
return cAdapter.testAdapter() + num;
}
@Transactional
@Override
public String testB2(Integer num) {
return null;
}
}
第三方依赖类
public interface CAdapter {
String testAdapter();
}
@Service
public class CAdapterImpl implements CAdapter {
@Override
public String testAdapter() {
return "this CAdapterImpl";
}
}
pom依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.1.9.RELEASE</version>
</dependency>
单元测试一:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext-test.xml"})
@TransactionConfiguration(defaultRollback = true)
@Transactional
public class BaseTest {
}
public class AServiceTest extends BaseTest{
@Autowired
private AServiceImpl aService;
@Mock
private CAdapter cAdapter;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
MockitoAnnotations.initMocks(cAdapter);
ReflectionTestUtils.setField(aService, "cAdapter", cAdapter);
Mockito.when(cAdapter.testAdapter()).thenReturn("mock");
}
@Test
public void testA() {
String result = aService.testA(3);
System.out.println(result);
}
}
// 输出:mock3
单元测试二:
public class BServiceTest extends BaseTest {
@Autowired
private BServiceImpl bService;
@Mock
private CAdapter cAdapter;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
MockitoAnnotations.initMocks(cAdapter);
ReflectionTestUtils.setField(bService, "cAdapter", cAdapter);
Mockito.when(cAdapter.testAdapter()).thenReturn("mock");
}
@Test
public void testB() {
String result = bService.testB(3);
System.out.println(result);
}
}
// 输出:this CAdapterImpl3
单元测试一符合预期结果,但单元测试二的输出结果有点出乎意料,按正常逻辑来说也应该输出“mock3”!
2.问题分析:
分析这个问题首先明确以下两点:
该工程对代理的生成方式是默认生成CGLIB代理,参考上面的配置文件
Spring类中若有方法标记了“@Transactional”,则Spring容器在启动时会自动为该方法所在类生成代理
明确以上两点可知:
BServiceImpl类testB2方法中标记了“@Transactional”,则在单元测试二中Spring容器会为BServiceImpl生成CGLIB代理,如下图:
而 ReflectionTestUtils.setField(bService, "cAdapter", cAdapter)会把cAdapter的mock对象设置到BServiceImpl实例属性中,而非代理类中。
单元测试一中,Spring容器不会为AServiceImpl生成代理,ReflectionTestUtils.setField(bService, "cAdapter", cAdapter)直接把AServiceImpl实例中的cAdapter属性替换为mock对象,所以输出的是“mock3”,如下图:3. 解决方案
要想单元测试二正确输出“mock3”,只需要获取其真正的代理,然后填充代理中的属性即可,改造如下:
public class BServiceTest extends BaseTest {
@Autowired
private BServiceImpl bService;
@Mock
private CAdapter cAdapter;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
MockitoAnnotations.initMocks(cAdapter);
try {
ReflectionTestUtils.setField(AopTargetUtils.getTarget(bService), "cAdapter", cAdapter);
} catch (Exception e) {
e.printStackTrace();
}
Mockito.when(cAdapter.testAdapter()).thenReturn("mock");
}
@Test
public void testB() {
String result = bService.testB(3);
System.out.println(result);
}
}
其中工具类AopTargetUtils:
public class AopTargetUtils {
public static Object getTarget(Object proxy) throws Exception {
if (!AopUtils.isAopProxy(proxy)) {
return proxy;//不是代理对象
}
if (AopUtils.isJdkDynamicProxy(proxy)) {
return getJdkDynamicProxyTargetObject(proxy);
} else {
return getCglibProxyTargetObject(proxy);
}
}
private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(proxy);
Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
return target;
}
private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
Field advised = aopProxy.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport) advised.get(aopProxy)).getTargetSource().getTarget();
return target;
}
}
方案二
可把spring-test版本升级到4.3.13.RELEASE版本,可见Spring开发人员知道有这个缺陷后,在后来的版本已经升级了,所以说如果有高版本的spring-test就不会出现以上问题,也算是一个学习过程。
相对于低版本的spring-test,ReflectionTestUtils多出了代理获取过程:
public static void setField(Object targetObject, Class<?> targetClass, String name, Object value, Class<?> type) {
Assert.isTrue(targetObject != null || targetClass != null,
"Either targetObject or targetClass for the field must be specified");
// 多出的代理获取过程
if (targetObject != null && springAopPresent) {
targetObject = AopTestUtils.getUltimateTargetObject(targetObject);
}
if (targetClass == null) {
targetClass = targetObject.getClass();
}
//......
}
public static <T> T getUltimateTargetObject(Object candidate) {
Assert.notNull(candidate, "Candidate must not be null");
try {
if (AopUtils.isAopProxy(candidate) && candidate instanceof Advised) {
return (T) getUltimateTargetObject(((Advised) candidate).getTargetSource().getTarget());
}
}
catch (Throwable ex) {
throw new IllegalStateException("Failed to unwrap proxied object", ex);
}
return (T) candidate;
}