一.要解决的问题:
spring环境中单元测试其实不是真正意义上的单元测试,真正意义上的单元测试应该是隔离所有的依赖,对当前业务实现业务逻辑测试;但是目前spring好像还没提供这样的解决方案,只能做依赖于环境的集成测试。比如:要测试A类,但是A类依赖B类和C类,这个时候我们必须保证B和C是完整的且是相对稳定的没太多bug的类.但是实际开发过程中,C类和B类可能是对数据库操作的Dao层或是对外接口层,这个时候我们在测试A类的时候业务B和C的环境或B或C都现在还没开发完成只是一个接口定义完成,这个时候就很难完成我们A类的测试了。
二.解决方案:
为了解决这个问题我们必须在测试的时候忽略B和C类,换句话说就是假象B和C都是可以运行或按我们预期返回结果的运行。我们利用mockito来掩饰我们测试类的所有的依赖。这样我们需要做到两点1.我们可以让B和C可以控制返回预期;2.B和C必须注入到spring中替换我们的测试类的依赖.
spring环境中单元测试其实不是真正意义上的单元测试,真正意义上的单元测试应该是隔离所有的依赖,对当前业务实现业务逻辑测试;但是目前spring好像还没提供这样的解决方案,只能做依赖于环境的集成测试。比如:要测试A类,但是A类依赖B类和C类,这个时候我们必须保证B和C是完整的且是相对稳定的没太多bug的类.但是实际开发过程中,C类和B类可能是对数据库操作的Dao层或是对外接口层,这个时候我们在测试A类的时候业务B和C的环境或B或C都现在还没开发完成只是一个接口定义完成,这个时候就很难完成我们A类的测试了。
二.解决方案:
为了解决这个问题我们必须在测试的时候忽略B和C类,换句话说就是假象B和C都是可以运行或按我们预期返回结果的运行。我们利用mockito来掩饰我们测试类的所有的依赖。这样我们需要做到两点1.我们可以让B和C可以控制返回预期;2.B和C必须注入到spring中替换我们的测试类的依赖.
- /**
- * 类SayServiceTests.java的实现描述:Mock demo
- *
- * @author hujf 2011-3-2 下午02:04:08
- */
- @ContextConfiguration(locations = { "/applicationContext.xml" })
- @RunWith(SpringJUnit4ClassRunner.class)
- @TestExecutionListeners({ org.springframework.test.context.support.DependencyInjectionAsMockitoTestExecutionListener.class })
- public class SayServiceTest {
- @Mock
- public SayDao sayDao;
- @Autowired
- public SayService sayService; // TODO 暂时用实现类
- @Test
- public void testSay() {
- // 1.设置预期行为 void
- when(sayDao.sayTo(null)).thenReturn("3");
- // 2.验证
- assertTrue(sayService.sayTo(null).equals("3"));
- }
- public SayDao getSayDao() {
- return sayDao;
- }
- public void setSayDao(SayDao sayDao) {
- this.sayDao = sayDao;
- }
- public SayService getSayService() {
- return sayService;
- }
- public void setSayService(SayService sayService) {
- this.sayService = sayService;
- }
- @Test
- public void testSayTo() {
- System.out.println("testSayTo...");
- // 1.设置预期行为
- when(sayDao.sayTo(any(Person.class))).thenAnswer(new Answer() {
- @Override
- public Object answer(InvocationOnMock invocation) throws Throwable {
- Object[] args = invocation.getArguments();
- // SayDao mock = (SayDao)invocation.getMock(); //Object mock = invocation.getMock();
- if (null != args && args.length > 0) {
- Person person = (Person) args[0];
- return person.getName();
- }
- return null;
- }
- });
- // 2.验证
- Person person = new Person();
- person.setId(11);
- person.setName("Leifeng");
- String s = sayService.sayTo(person);
- System.out.println(s);
- assertSame("Leifeng", s);
- }
- @Test
- public void testSaySomething() {
- System.out.println("testSaySomething...");
- // 1.设置预期行为
- when(sayDao.saySomething(anyString(), any(Person.class), anyString())).thenAnswer(new Answer<String>() {
- @Override
- public String answer(InvocationOnMock invocation) throws Throwable {
- Object[] args = invocation.getArguments();
- if (null != args && args.length > 0) {
- String hello = (String) args[0];
- Person person = (Person) args[1];
- String msg = (String) args[2];
- return hello + "," + person.getName() + "(" + person.getId() + ")" + ":" + msg;
- // SayDao dao = new SayDaoImpl();
- // return dao.saySomething(hello, person, msg);
- }
- return null;
- }
- });
- // 2.验证
- Person person = new Person();
- person.setId(12);
- person.setName("Leifeng");
- String s = sayService.saySomething("Welcome", person, "handsome guy!");
- System.out.println(s);
- assertNotNull(s);
- }
- @Test
- public void testQueryPerson() {
- // 1.预置预期行为
- List<Person> personList = new ArrayList<Person>();
- // 初始化List《Person》
- for (int i = 0; i < 10; i++) {
- Person person = new Person();
- person.setId(i + 1);
- person.setName("name" + i);
- personList.add(person);
- }
- when(sayDao.queryPerson(any(Person.class))).thenReturn(personList);
- // 2.验证
- Person query = new Person();
- query.setId(13);
- query.setName("Leifeng");
- List<Person> list = sayService.queryPerson(query);
- assertTrue(10 == list.size());
- // 重要(根据具体业务设计)
- assertTrue(3 == list.get(3).getFlag());
- }
- }
/**
* 类SayServiceTests.java的实现描述:Mock demo
*
* @author hujf 2011-3-2 下午02:04:08
*/
@ContextConfiguration(locations = { "/applicationContext.xml" })
@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners({ org.springframework.test.context.support.DependencyInjectionAsMockitoTestExecutionListener.class })
public class SayServiceTest {
@Mock
public SayDao sayDao;
@Autowired
public SayService sayService; // TODO 暂时用实现类
@Test
public void testSay() {
// 1.设置预期行为 void
when(sayDao.sayTo(null)).thenReturn("3");
// 2.验证
assertTrue(sayService.sayTo(null).equals("3"));
}
public SayDao getSayDao() {
return sayDao;
}
public void setSayDao(SayDao sayDao) {
this.sayDao = sayDao;
}
public SayService getSayService() {
return sayService;
}
public void setSayService(SayService sayService) {
this.sayService = sayService;
}
@Test
public void testSayTo() {
System.out.println("testSayTo...");
// 1.设置预期行为
when(sayDao.sayTo(any(Person.class))).thenAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
// SayDao mock = (SayDao)invocation.getMock(); //Object mock = invocation.getMock();
if (null != args && args.length > 0) {
Person person = (Person) args[0];
return person.getName();
}
return null;
}
});
// 2.验证
Person person = new Person();
person.setId(11);
person.setName("Leifeng");
String s = sayService.sayTo(person);
System.out.println(s);
assertSame("Leifeng", s);
}
@Test
public void testSaySomething() {
System.out.println("testSaySomething...");
// 1.设置预期行为
when(sayDao.saySomething(anyString(), any(Person.class), anyString())).thenAnswer(new Answer<String>() {
@Override
public String answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
if (null != args && args.length > 0) {
String hello = (String) args[0];
Person person = (Person) args[1];
String msg = (String) args[2];
return hello + "," + person.getName() + "(" + person.getId() + ")" + ":" + msg;
// SayDao dao = new SayDaoImpl();
// return dao.saySomething(hello, person, msg);
}
return null;
}
});
// 2.验证
Person person = new Person();
person.setId(12);
person.setName("Leifeng");
String s = sayService.saySomething("Welcome", person, "handsome guy!");
System.out.println(s);
assertNotNull(s);
}
@Test
public void testQueryPerson() {
// 1.预置预期行为
List<Person> personList = new ArrayList<Person>();
// 初始化List《Person》
for (int i = 0; i < 10; i++) {
Person person = new Person();
person.setId(i + 1);
person.setName("name" + i);
personList.add(person);
}
when(sayDao.queryPerson(any(Person.class))).thenReturn(personList);
// 2.验证
Person query = new Person();
query.setId(13);
query.setName("Leifeng");
List<Person> list = sayService.queryPerson(query);
assertTrue(10 == list.size());
// 重要(根据具体业务设计)
assertTrue(3 == list.get(3).getFlag());
}
}
DependencyInjectionAsMockitoTestExecutionListener类是在spring-test中的DependencyInjectionTestExecutionListener基础上扩展的一个结合mock的测试监听器。我们在测试的时候可以用注解TestExecutionListeners指定这个监听器来实现单元测试
- package org.springframework.test.context.support;
- import java.lang.annotation.Annotation;
- import java.lang.reflect.Field;
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
- import java.util.ArrayList;
- import java.util.List;
- import static org.mockito.Mockito.mock;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.test.context.TestContext;
- /**
- * @author yanglin lv
- */
- public class DependencyInjectionAsMockitoTestExecutionListener extends DependencyInjectionTestExecutionListener {
- private static String SETTER = "set";
- private static String GETTER = "get";
- @Override
- protected void injectDependencies(final TestContext testContext) throws Exception {
- super.injectDependencies(testContext);
- Object bean = testContext.getTestInstance();
- Class[] mockClass = getMockClass(bean.getClass());
- Method[] methods = bean.getClass().getDeclaredMethods();
- Class clz = bean.getClass();
- Object instance = null;
- List<MockObjectMap> objs = new ArrayList<MockObjectMap>();
- autowireMockBean(clz, bean, objs);
- List<Object> stubObjs = getStubInstance(clz, bean);
- autowireMockBeanForSpring(stubObjs, objs);
- }
- private void autowireMockBeanForSpring(List<Object> stubObjs, List<MockObjectMap> objs)
- throws IllegalArgumentException,
- IllegalAccessException,
- InvocationTargetException {
- for (Object object : stubObjs) {
- Class claz = object.getClass();
- do {
- for (Method method : claz.getDeclaredMethods()) {
- if (method.getName().startsWith(SETTER)) {
- for (MockObjectMap mockObjectMap : objs) {
- Object obj = method.getGenericParameterTypes()[0];
- if (obj instanceof java.lang.reflect.Type
- && mockObjectMap.getType().getName().equalsIgnoreCase(((Class) obj).getName())) {
- method.invoke(object, mockObjectMap.getObj());
- continue;
- }
- }
- }
- }
- claz = claz.getSuperclass();
- } while (!claz.equals(Object.class));
- }
- }
- private void autowireMockBean(Class clz, Object bean, List<MockObjectMap> objs) throws IllegalArgumentException,
- IllegalAccessException {
- for (Field field : clz.getFields()) {
- Annotation[] mockAnnotations = field.getAnnotations();
- for (Annotation annotation : mockAnnotations) {
- if (annotation instanceof org.mockito.Mock) {
- MockObjectMap mockObjectMap = new MockObjectMap();
- objs.add(mockObjectMap);
- mockObjectMap.setType(field.getType());
- mockObjectMap.setObj(mock(field.getType()));
- field.setAccessible(true);
- field.set(bean, mockObjectMap.getObj());
- continue;
- }
- }
- }
- }
- /**
- * 取得测试类中所有的mock对象的类型
- *
- * @param clazz
- * @return
- */
- private Class[] getMockClass(Class claz) {
- List<Class> clasList = new ArrayList<Class>();
- Field[] fields = claz.getDeclaredFields();
- for (Field field : fields) {
- Annotation[] mockAnnotations = field.getAnnotations();
- for (Annotation annotation : mockAnnotations) {
- if (annotation instanceof org.mockito.Mock) {
- clasList.add(field.getType());
- continue;
- }
- }
- }
- return clasList.toArray(new Class[0]);
- }
- /**
- * 取得测试类中测试桩类
- *
- * @param clazz
- * @return
- * @throws InvocationTargetException
- * @throws IllegalAccessException
- * @throws IllegalArgumentException
- */
- private List<Object> getStubInstance(Class clazz, Object bean) throws IllegalArgumentException,
- IllegalAccessException, InvocationTargetException {
- List<Object> objList = new ArrayList<Object>();
- Field[] fields = clazz.getDeclaredFields();// 测试类中所有的域名
- Method[] methods = clazz.getDeclaredMethods();
- for (Field field : fields) {
- Annotation[] mockAnnotations = field.getAnnotations();
- for (Annotation annotation : mockAnnotations) {
- if (annotation instanceof Autowired) {
- for (Method method : methods) {
- String name = field.getName();
- if (method.getName().startsWith(GETTER) && method.getName().substring(3).equalsIgnoreCase(name)) {
- objList.add(method.invoke(bean, null)); // 将所有的测试桩类放在objList
- }
- }
- }
- }
- }
- return objList;
- }
- private class MockObjectMap {
- private Object obj;
- private Class<?> type;
- public Object getObj() {
- return obj;
- }
- public void setObj(Object obj) {
- this.obj = obj;
- }
- public Class<?> getType() {
- return type;
- }
- public void setType(Class<?> type) {
- this.type = type;
- }
- }
- }
package org.springframework.test.context.support;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.Mockito.mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestContext;
/**
* @author yanglin lv
*/
public class DependencyInjectionAsMockitoTestExecutionListener extends DependencyInjectionTestExecutionListener {
private static String SETTER = "set";
private static String GETTER = "get";
@Override
protected void injectDependencies(final TestContext testContext) throws Exception {
super.injectDependencies(testContext);
Object bean = testContext.getTestInstance();
Class[] mockClass = getMockClass(bean.getClass());
Method[] methods = bean.getClass().getDeclaredMethods();
Class clz = bean.getClass();
Object instance = null;
List<MockObjectMap> objs = new ArrayList<MockObjectMap>();
autowireMockBean(clz, bean, objs);
List<Object> stubObjs = getStubInstance(clz, bean);
autowireMockBeanForSpring(stubObjs, objs);
}
private void autowireMockBeanForSpring(List<Object> stubObjs, List<MockObjectMap> objs)
throws IllegalArgumentException,
IllegalAccessException,
InvocationTargetException {
for (Object object : stubObjs) {
Class claz = object.getClass();
do {
for (Method method : claz.getDeclaredMethods()) {
if (method.getName().startsWith(SETTER)) {
for (MockObjectMap mockObjectMap : objs) {
Object obj = method.getGenericParameterTypes()[0];
if (obj instanceof java.lang.reflect.Type
&& mockObjectMap.getType().getName().equalsIgnoreCase(((Class) obj).getName())) {
method.invoke(object, mockObjectMap.getObj());
continue;
}
}
}
}
claz = claz.getSuperclass();
} while (!claz.equals(Object.class));
}
}
private void autowireMockBean(Class clz, Object bean, List<MockObjectMap> objs) throws IllegalArgumentException,
IllegalAccessException {
for (Field field : clz.getFields()) {
Annotation[] mockAnnotations = field.getAnnotations();
for (Annotation annotation : mockAnnotations) {
if (annotation instanceof org.mockito.Mock) {
MockObjectMap mockObjectMap = new MockObjectMap();
objs.add(mockObjectMap);
mockObjectMap.setType(field.getType());
mockObjectMap.setObj(mock(field.getType()));
field.setAccessible(true);
field.set(bean, mockObjectMap.getObj());
continue;
}
}
}
}
/**
* 取得测试类中所有的mock对象的类型
*
* @param clazz
* @return
*/
private Class[] getMockClass(Class claz) {
List<Class> clasList = new ArrayList<Class>();
Field[] fields = claz.getDeclaredFields();
for (Field field : fields) {
Annotation[] mockAnnotations = field.getAnnotations();
for (Annotation annotation : mockAnnotations) {
if (annotation instanceof org.mockito.Mock) {
clasList.add(field.getType());
continue;
}
}
}
return clasList.toArray(new Class[0]);
}
/**
* 取得测试类中测试桩类
*
* @param clazz
* @return
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
private List<Object> getStubInstance(Class clazz, Object bean) throws IllegalArgumentException,
IllegalAccessException, InvocationTargetException {
List<Object> objList = new ArrayList<Object>();
Field[] fields = clazz.getDeclaredFields();// 测试类中所有的域名
Method[] methods = clazz.getDeclaredMethods();
for (Field field : fields) {
Annotation[] mockAnnotations = field.getAnnotations();
for (Annotation annotation : mockAnnotations) {
if (annotation instanceof Autowired) {
for (Method method : methods) {
String name = field.getName();
if (method.getName().startsWith(GETTER) && method.getName().substring(3).equalsIgnoreCase(name)) {
objList.add(method.invoke(bean, null)); // 将所有的测试桩类放在objList
}
}
}
}
}
return objList;
}
private class MockObjectMap {
private Object obj;
private Class<?> type;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public Class<?> getType() {
return type;
}
public void setType(Class<?> type) {
this.type = type;
}
}
}
总结:这种方式可以真正的用spring来实现TDD面向接口的测试方案,对依赖的类做到完全屏蔽,对目前测试类和mock类设置期望输出简单实现