原则
单元测试FIRST原则如下
- 快速(fast):单元测试应该是快速运行的,否则将耗费掉很多开发/部署时间。
- 隔离(isolated):不同的测试用例之间是隔离的。一个测试不会依赖另一个测试。
- 可重复(repeatable):单元测试是可重复运行的,且在重复运行时,单元测试总能给出相同的结果(系统环境无关)。
- 自我验证(self-validating):单元测试可以验证它们的结果,当它们全部通过时,给出一个简单的“OK”报告,当它们失败时,需要输出描述简明的细节。
- 及时(timely):程序员在代码上线前,应该及时地编写它们,以防止bug。
Junit+Mockito+H2数据库
整体思路是二方服务的API、配置型数据均采用mock方式,结合使用H2内存数据库来初始化数据达到单元测试的目的。
二方服务的API采用mock方式
为了减小单元测试执行时间,需要去除二方服务api的加载并以mock对象替代,api mock的方式一般是在Spring启动时进行mock注入,即mock对象替换真实的对象。这里可使用Spring自带的 @TestExecutionListeners 注解来进行统一mock二方服务api,执行步骤如如下:
- 创建TestContextManager
- 依次执行各个 AbstractTestExecutionListener 实例的 beforeTestClass 方法
- 依次执行各个 AbstractTestExecutionListener 实例的 prepareTestInstance 方法
- Spring加载并解析XML,在此过程中执行BeanFactoryPostProcessor
- 执行一个测试方法时,依次执行各个 AbstractTestExecutionListener 实例的 beforeTestMethod 方法
- 执行测试方法
- 依次执行各个 AbstractTestExecutionListener 实例的 afterTestMethod 方法
上述方式可以达到按@Mock注解标记的对象能统一进行对象mock,不加载外部环境,做到环境隔离、单元测试运行速度快等特点。详细代码如下:
public class MockitoBeansPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
Map<Class<?>, MockitoBeansTestExecutionListener.MockBeanWrapper> allMockBeans = MockitoBeansTestExecutionListener.resolvedAllMockBeans();
for (Map.Entry<Class<?>, MockitoBeansTestExecutionListener.MockBeanWrapper> mockBeanWrapperEntry : allMockBeans.entrySet()) {
beanFactory.registerResolvableDependency(mockBeanWrapperEntry.getKey(), mockBeanWrapperEntry.getValue().getMockObject());
beanFactory.registerSingleton(mockBeanWrapperEntry.getValue().getBeanName(), mockBeanWrapperEntry.getValue().getMockObject());
}
}
}
public class MockitoBeansTestExecutionListener extends DependencyInjectionTestExecutionListener {
private static Map<Class<?>, MockBeanWrapper> mockBeans = new ConcurrentHashMap<>();
private static Map<Class<?>, List<Field>> injectMockBeans = new ConcurrentHashMap<>();
private static boolean hasInitialized = false;
public static Map<Class<?>, MockBeanWrapper> resolvedAllMockBeans() {
Assert.isTrue(hasInitialized);
return Collections.unmodifiableMap(mockBeans);
}
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
Field[] declaredFields = testContext.getTestClass().getDeclaredFields();
// 拿到父类的Mock方法
Field[] superClassFields = testContext.getTestClass().getSuperclass().getDeclaredFields();
List<Field> mockFields = Lists.newArrayList(declaredFields);
if (superClassFields.length > 0) {
mockFields.addAll(Lists.newArrayList(superClassFields));
}
//将需要mock的对象创建出来
for (Field field : mockFields) {
Mock mockAnnon = field.getAnnotation(Mock.class);
if (mockAnnon != null) {
field.setAccessible(true);
MockBeanWrapper wrapper = new MockBeanWrapper();
Class<?> type = field.getType();
wrapper.setMockObject(Mockito.mock(type));
wrapper.setBeanType(type);
wrapper.setBeanName(StringUtils.isEmpty(mockAnnon.value()) ? field.getName() : mockAnnon.value());
mockBeans.putIfAbsent(wrapper.getBeanType(), wrapper);
injectMockBeans.compute(testContext.getTestClass(), (targetClass, waitInjectFields) -> {
if (waitInjectFields == null) {
waitInjectFields = new ArrayList<>();
}
waitInjectFields.add(field);
return waitInjectFields;
});
}
}
hasInitialized = true;
}
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
Object testInstance = testContext.getTestInstance();
List<Field> fields = injectMockBeans.get(testContext.getTestClass());
if (fields != null) {
for (Field field : fields) {
field.setAccessible(true);
field.set(testInstance, mockBeans.get(field.getType()).getMockObject());
}
}
}
@Data
public class MockBeanWrapper {
private String beanName;
private Class<?> beanType;
private Object mockObject;
}
}
// 单元测试基类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:promotion/config/application-unittest.xml"})
@TestExecutionListeners({MockitoBeansTestExecutionListener.class,
TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class})
public abstract class AbstractTest {
@Mock
protected LogisticService logisticService;
@Mock
protected ItemBatchSpecService itemBatchSpecService;
//......
}
H2内存数据库进行数据初始化
H2数据库是嵌入式的内存型数据库,其语法与MySQL语法非常接近,非常适用于单元测试数据准备场景。所以需要达到属于本应用负责的数据直接依赖H2数据库存储,单个用例使用Spring@sql注解单独准备数据,即应用负责的方法不进行mock,从上层一直到数据库真实调用。详细配置如下:
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:common/common_schema.sql"/>
</jdbc:embedded-database>
<bean id="h2SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:common/mybatis-config.xml"/>
<property name="mapperLocations">
<list>
<value>classpath:mapper/brule/Promotion*.xml</value>
</list>
</property>
</bean>
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.yt.smc.dal.flashbuy.mapper,com.yt.smc.dal.brule.mapper"/>
<property name="sqlSessionFactoryBeanName" value="h2SqlSessionFactory"/>
</bean>
单元测试示例
单元测试类只需要继承单元测试基类AbstractTest即可,示例代码如下:
public class NoBatchPlaceOrderTest extends AbstractTest {
@Autowired
private TradeService tradeService;
@Sql("classpath:promotion/db/trade/placeorder/nobatch_place_order_01.sql")
@Test
public void testplaceOrderFinishTest1() {
Mockito.when(itemPriceService.skuPrice(any())).thenReturn(getNoBatchSkuPriceMap());
SmcResultData<List<CouponOwnerShareDTO>> smcResultData = new SmcResultData<>();
smcResultData.setData(Lists.newArrayList());
Mockito.when(couponTradeService.shareCouponOwner(any(), any())).thenReturn(smcResultData);
SmcResultData<PaceOrderFinishReDTO> paceOrderFinishResult = new SmcResultData<>();
PaceOrderFinishReDTO paceOrderFinishReDTO = new PaceOrderFinishReDTO();
paceOrderFinishResult.setData(paceOrderFinishReDTO);
Mockito.when(promotionPlaceExecuteService.placeOrderFinishPromotion(any(), any())).thenReturn(paceOrderFinishResult);
Mockito.when(itemSqueryAdapter.listItemByIds(Mockito.any())).thenReturn(getItemDetailList());
PlaceOrderRequestDTO requestDTO = buildPlaceOrderRequest1();
PlaceOrderResponseDTO responseDTO = tradeService.placeOrderFinish(requestDTO);
System.out.println(JSON.toJSONString(responseDTO));
Assert.assertNotNull(responseDTO);
Assert.assertEquals(1, responseDTO.getPromotionPlaceOrderDTOS().size());
Assert.assertEquals(16900, responseDTO.getPromotionPlaceOrderDTOS().get(0).getOrderPrice().longValue());
Assert.assertEquals(1, responseDTO.getPromotionPlaceOrderDTOS().get(0).getPromotionPlaceActivityDTOs().size());
Assert.assertEquals(3000, responseDTO.getPromotionPlaceOrderDTOS().get(0).getPromotionPlaceActivityDTOs().get(0).getActivityCreatePrice().longValue());
}
Spock+Mockito+H2数据库
采用Spock的思想基本同Juit思想一样,区别点在使用groovy语言进行写单元测试。使用groovy进行单元测试基于行为驱动开发(BDD)的思想,单元测试过程中需要将用例对应为given、when和then,与Junit相比这种方式更加贴近业务表达。首先引入依赖如下(注意要选择相匹配的版本,版本不一致可能会引起单元测试失败):
<!--Spock测试框架-->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.3-groovy-2.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.4</version>
</dependency>
再基于上述mock思路创建AbstractSpockTest基类,如下:
@ContextConfiguration(locations = "classpath:promotion/config/application-unittest.xml")
@TestExecutionListeners([
MockitoBeansTestExecutionListener.class,
TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class
])
abstract class AbstractSpockTest extends Specification {
@Mock
protected ItemBatchSpecService itemBatchSpecService;
//......
}
单元测试示例如下:
class UserTaskServiceSpockTest extends AbstractSpockMockTest {
@Autowired
private UserTaskService userTaskService;
@Sql("classpath:db/share/query_share_detail_info.sql")
@Unroll
def "被分享人扫分享码详情单元测试"() {
given:
Mockito.when(activityService.checkActivityById(any(), any())).thenReturn(getCheckReturnResult())
Mockito.when(activityService.checkUser(any(), any(), any())).thenReturn(getCheckReturnResult())
Mockito.when(iSnsUserServiceWrapper.getSnsUserById(any())).thenReturn(getSnsTaoBaoUserDTO())
ShareDetailQueryRequest shareDetailQueryRequest = new ShareDetailQueryRequest(shareCode: shareCode, userId: userId)
expect:
ServiceResult<ScanShareResultVO> result = userTaskService.queryShareDetailInfo(shareDetailQueryRequest)
result.getData().getSelf() == self
result.getData().getSourceUserId() == sourceUserId
where:
shareCode | userId | self | sourceUserId
"da0f4a99c41ff8cc3634830552239c8fb5503076ab" | 3L | false | 5L
"da0f4a99c41ff8cc3634830552239c899e683076ab" | 4L | true | 5L
}
}
总结
两种实现方式原理相同,从风格上讲"Spock+Mockito+H2数据库"这种方式代码量更少、更贴近业务表达,推荐!