原则

单元测试FIRST原则如下

  • 快速(fast):单元测试应该是快速运行的,否则将耗费掉很多开发/部署时间。
  • 隔离(isolated):不同的测试用例之间是隔离的。一个测试不会依赖另一个测试。
  • 可重复(repeatable):单元测试是可重复运行的,且在重复运行时,单元测试总能给出相同的结果(系统环境无关)。
  • 自我验证(self-validating):单元测试可以验证它们的结果,当它们全部通过时,给出一个简单的“OK”报告,当它们失败时,需要输出描述简明的细节。
  • 及时(timely):程序员在代码上线前,应该及时地编写它们,以防止bug。

Junit+Mockito+H2数据库

uinttest框架如何实现选择特定的一条测试用例多次测试_单元测试

 

整体思路是二方服务的API、配置型数据均采用mock方式,结合使用H2内存数据库来初始化数据达到单元测试的目的。

二方服务的API采用mock方式

为了减小单元测试执行时间,需要去除二方服务api的加载并以mock对象替代,api mock的方式一般是在Spring启动时进行mock注入,即mock对象替换真实的对象。这里可使用Spring自带的 @TestExecutionListeners 注解来进行统一mock二方服务api,执行步骤如如下:

uinttest框架如何实现选择特定的一条测试用例多次测试_H2_02

  1. 创建TestContextManager
  2. 依次执行各个 AbstractTestExecutionListener 实例的 beforeTestClass 方法
  3. 依次执行各个 AbstractTestExecutionListener 实例的 prepareTestInstance 方法
  4. Spring加载并解析XML,在此过程中执行BeanFactoryPostProcessor
  5. 执行一个测试方法时,依次执行各个 AbstractTestExecutionListener 实例的 beforeTestMethod 方法
  6. 执行测试方法
  7. 依次执行各个 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数据库"这种方式代码量更少、更贴近业务表达,推荐!