单元测试说明
目前单元测试使用spring-boot-starter-test自带的Junit+Mockito框架,使用PowerMock针对Mockito进行功能增强。
单元测试需要保证以下几点:
- 不依赖外部系统
- 单元测试足够“单元”,避免流程过长的测试逻辑
- 测试方法的数据操作不影响真实数据源的数据现状
- 每个测试方法都要有verify或者Assert的验证或断言的操作,否则都是无效的测试方式
- 一些依赖外部系统的调用,或不需要每次单元测试都执行的测试类或测试方法,及时使用
@Ignore
,避免mvn test时执行单元测试异常
Mockito的问题
- mock数据不友好,数据结果对比也不友好的问题。
- 不支持final/static的类或方法的mock操作(已使用PowerMock增强字节码方式解决)
- 但PowerMockRunner会重新加载Spring Context,如果配合SpringRunner使用,在集成单元测试阶段,
会有Context重复加载问题。例如xxljob的端口被占用,因为SpringRunner在集成测试中会使用context缓存,
以减少每个测试类重复加载上下文的消耗。
- 但PowerMockRunner会重新加载Spring Context,如果配合SpringRunner使用,在集成单元测试阶段,
- 后续会进行其他单元测试框架调研的工作,主要考虑mock数据的生成和结果对比的友好程度。
Mockito基本使用说明
Mockito主要针对mock实际对象的行为,进行bean之间的依赖解耦处理。主要通过以下几个注解和方法进行mock操作。
1. @InjectMocks
@InjectMocks
标记一个对象需要进行mock对象的注入,mock对象指被 @Mock
或者 @Spy
注解修饰的bean(后面说明这两个注解)。
@InjectMocks
修饰的对象一定是非接口对象,否则无法确定bean在初始化时需要mock哪些属性或方法。
@InjectMocks
注入mock对象的方式有两种:
- 添加
MockitoAnnotations.initMocks(this);
表示改测试类需要初始化mock对象并注入到@InjectMocks
对象中。 - 如果使用了PowerMockRunner,则PowerMock会自动进行mock对象的注入,无须额外添加注入。测试类继承
com.jdh.fuhsi.BasePowerMockRunner
即可。
(实际就是给类添加@RunWith(PowerMockRunner.class)
)
例子1:CiticbankPushCustomerServiceImplTest
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest
@Transactional
@Rollback
public abstract class BaseSpringJUnitRunner {
}
public class CiticbankPushCustomerServiceImplTest extends BaseSpringJUnitRunner {
@InjectMocks
private CiticbankPushCustomerServiceImpl citicbankPushCustomerService;
@Mock
private CiticbankHttpServerTemplate citicbankHttpServerTemplate;
@Spy
private CiticbankRegistNotifyServiceImpl citicbankRegistNotifyService;
@Spy
private JdhHttpServer jdhHttpServer;
@Before
public void setUp() {
// 注入@Mock,@Spy对象到@InjectMocks中
MockitoAnnotations.initMocks(this);
request = JSONObject.parseObject(REQ_MESSAGE, RegisterApplyRequest.class);
// 模拟回调成功
Resp resp = RespUtil.success("mockSuccess");
Mockito.doReturn(JSON.toJSONString(resp)).when(jdhHttpServer).simpleSend(anyString(), anyString(), any());
}
@Test
public void testSuccessRequest() {
// 具体的测试方法
....
}
例子2:ChannelRequestRetryJobHandlerTest
// 基类标记了PowerMockRunner,使用PowerMock框架
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*"})
public abstract class BasePowerMockRunner {
}
@PrepareForTest(CommonUtil.class)
public class ChannelRequestRetryJobHandlerTest extends BasePowerMockRunner {
// PowerMock会自动将下面的mock对象注入到channelRequestRetryJobHandler中
@InjectMocks
@Spy
private ChannelRequestRetryJobHandler channelRequestRetryJobHandler = new ChannelRequestRetryJobHandler();
/**
* 模拟server
*/
@Mock
private DefaultHttpServerTemplate defaultHttpServerTemplate;
@Mock
private ChannelRequestLogService channelRequestLogService;
@Mock
private FileInfoNoticeService fileInfoNoticeService;
...
}
2. @Mock
@Mock
会将对象的所有方法进行mock处理,一律默认返回null。可以对mock对象进行Mockito.when(…)设置模拟方法执行操作,
或Mcokito.verify(…)对mock对象的行为进行验证。
3. @Spy
@Spy
标记的对象也是一个mock对象,不同的是,默认所有方法都返回原始对象的方法,只有被Mockito.when(…)进行stub之后的方法才会走mock方法。
同样也可执行Mcokito.verify(…)对mock对象的行为进行验证。
搭配@Autowired使用,可以将SpringBean注入到@InjectMocks
中,同时还能对该对象进行Mockito.verify操作。
4. Mockito.when(…)
上述的注解是针对mock对象初始化,mock对象行为主要通过Mockito.when(…)方式进行,可以模拟方法返回,模拟异常等。模拟的行为称为stub。
以下用一个模拟返回方式,针对@Mock和@Spy的两种mock对象差异说明使用方式。
以下都是针对一个httpServer模拟返回成功对象的操作。
@Spy:
@Spy
private HttpServer httpServer;
...
Mockito.doReturn(JSON.toJSONString(resp)).when(httpServer).simpleSend(anyString(), anyString(), any());
@Mock:
@Mock
private CiticbankHttpServerTemplate citicbankHttpServerTemplate;
...
Response successResp = CommonUtil.buildSuccessResponse("mockSuccess");
Mockito.when(citicbankHttpServerTemplate.packageRequest(any(), any(), any(), anyBoolean())).thenReturn(successResp);
doReturn和thenReturn效果一致,区别在于前者不会真的执行mock对象对应的方法,而后者会执行mock对象的方法。
由于@Mock方式创建的对象所有方法都返回null,因此不会执行到真实方法;而@Spy如果没有进行stub操作,会执行真实方法。
stub的匹配规则:例子中出现的anyXXX(),表示这个mock方法在被何种入参场景调用时才会触发。
** null参数只能用any()识别,不能用anyString()
5. Mockito.verify(…)
每个单元测试的最后,都要针对一些数据进行Assert断言操作,例如
Assert.assertEquals("expected result", mockDate.getResult());
来断言模拟数据的某些数据状态是否符合预期。
针对mock对象的行为验证,比如mock方法执行次数,是否抛出异常,等等,则可通过Mockito.verify(…)进行验证。
例如,以下验证mockObject这个对象的retry方法,匹配参数为any(),即任意对象,至少在前面的测试方法里至少被调用过一次:
@Mock
private T mockObject;
...测试方法逻辑...
Mockito.verify(mockObject, Mockito.atLeastOnce()).retry(any());
static/final方法的Mock方式
Mockito本身不支持static/final方法的mock,目前使用PowerMock增强Mockito的处理。PowerMock的缺点前面提到,因此使用时不建议搭配SpringRunner使用,否则集成单元测试执行会失败。
static/final方法的mock例子:
// 测试基类,也可以直接在对应测试类上使用下面的注解,效果相同
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*"})
public abstract class BasePowerMockRunner {
}
// 说明要mock的static/final方法所属的类
@PrepareForTest(CommonUtil.class)
public class ChannelRequestRetryJobHandlerTest extends BasePowerMockRunner {
/**
* 模拟server
*/
@Mock
private DefaultHttpServerTemplate defaultHttpServerTemplate;
@Before
public void setUp() {
// 模拟返回静态方法,注入对应的Bean
PowerMockito.mockStatic(CommonUtil.class);
MockitoAnnotations.initMocks(this);
// 基础数据初始化
PowerMockito.when(CommonUtil.getBean(anyString())).thenReturn(defaultHttpServerTemplate);
}
}
Groovy使用demo
def spyHandler = Spy(AutoEffectiveFactorQuotaHandler)
直接使用mock的service 然后测试里面的方法