单元测试问题一览

    随着工程的越来越大,导致开发自测也越来越麻烦。工程大带来两大问题

  • 依赖越来越多,很多应用上下游关系链条特别长。只要有一个依赖在日常环境不具备,则日常不可用,只能发布到预发测试
  • 依赖多了后,每次启动都要加载不少东西。一次启动、发布少则几分钟,多的也要20来分钟,在预发测试非常耗时

曾经做过一段时间的商家群聊业务就是这个情况,感觉整个人效率下降了不少。那段时间开发需求,为了完成只能靠大量加班来完成。

一般使用的土办法是,对需要测试的类,注入的依赖bean注掉,依赖的数据也直接mock掉,或者直接写代码new出需要的依赖结果。这样测试功能与业务功能混在一下,有时可能忘记改回来。或者更加彻底的是把要测试的代码拿出来,测试完成再写回去,但不够便捷。

也有很多自动化测试框架、数据mock工具,但开发过程中追求的是速度与便捷。看了些都不太贴合开发人员开发过程中的自测。

其实需要解决的核心问题就是mock掉依赖、解决启动慢的问题,这样才能解放开发的效率。

一方面想让自己从这种境况中出来,一方面看是不是可以提高团队效率。于是构思写一个工具来解决掉这两个核心问题。

spring bean 生命周期大致分为实例化BeanFactoryPostProcessor、执行bean的构造器、为bean注入属性、执行初始化方法,其中有很多扩展点。

在实现BeanFactoryPostProcessor时注入进来的ConfigurableListableBeanFactory的类中,可对BeanDefinition进行操作,而BeanDefinition类正是解析xml中<bean ..../> 对应bean代码形式的定义。那么这个地方就可以实现对定义的bean增删改查(一下子变得low了),这对它操作也直接影响后后续的构造、初始化。

在执行bean的构造器的这一阶段,实现InstantiationAwareBeanPostProcessor的方法postProcessBeforeInstantiation返回的结果可替换spring new一个对象的操作。而这也让我们在传统spring提供的常规aop操作之外,提供了插入代理代码的一种路径。

基于以上两点,似乎就可以解决那两个问题。将多余的BeanDefinition删除掉,再用代理将需要mock的bean、方法替换掉。

以下是构思整体的一个流程图

  • 执行单元测试阶段

这个阶段实现了 ChironJunit4Runner 继承 Runner启动单元测试,以便分析测试单元的依赖,以及根据测试单元类注解加载mock数据。

分析的产出是得到单元直接依赖的bean(一级依赖:往往就是我们测试的bean),以及这些bean的依赖(二级依赖:往往就是需要mock的bean),现即假设一级依赖为需要测试的代码,二级依赖为需要mock的代码。

  • 操作bean的定义

在这一步实现BeanFactoryPostProcessor接口,利用注入进来的ConfigurableListableBeanFactory,将一级依赖、必要的bean(如spring自身的、影响启动的)以外的BeanDefinition。由于这一些是在生命周期比较靠前的阶段,可以排掉大部分bean的定义,直接省掉了他们的实例化、初始化时间。

  • 实例化bean/mock代码织入

继承InstantiationAwareBeanPostProcessorAdapter类重写postProcessBeforeInstantiation方法。这个只有一级依赖、必要的bean能到这,这部分直接使用spring自身的实例化。但这一步要分析出一级依赖的依赖即二级依赖,并将二级依赖的BeanDefinition再写入到spring容器中(已经过修改并非之前的定义)。对于新加的BeanDefinition,spring会自再次进行这一步。这时即可使用代理完全替换掉原来的类行为并织入mock代码。

至此理论上完成可行。

团队业务方向调整,新业务基本都是新起的工程对这个需求也没有那么急需。构思其实在之前项目时已经实现部分功能,但基于兴趣,还是在业余时间将他写完。

经过实践亲测构思可行,目前已经将这个测试组件写是工程代码里,计划下一步抽出成一个jar包,到时再给大家分享这个二方包的使用方法及一些心得体验。

这个自测小工具的核心是
  • 使用单元测试工具来快速测试业务逻辑,不需要频繁重启应用
  • 只加载你测试要用到的bean及其依赖树,不需要的一律不加载
  • 懒加载所有的bean

工具我已经打成jar包

        <dependency>
            <groupId>com.taobao.brand.component</groupId>
            <artifactId>chiron</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <scope>test</scope>
        </dependency>复制代码

使用方法
  • 使用ChironSpringJunitRunner或者ChironJunit4Runner来启动单元测试

示例

@RunWith(PandoraBootRunner.class)
@DelegateTo(ChironSpringJunitRunner.class)
@ContextConfiguration({"/module-test-context.xml"})
public abstract class BaseSpringTest {

    public void setMtopLoginUserInfo(long userId,String nick,String deviceId) {
        MessageRequest request = new MessageRequest();
        request.setUserId(userId);
        request.setNick(nick);
        request.setDeviceId(deviceId);
        MtopContext.putMessageRequest(request);
    }

}复制代码

  • 配置spring加载工具

示例

    <tx:annotation-driven />
    <context:annotation-config/>

    <bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="locations">
            <list>
                <value>classpath:/module-test-context.properties</value>
            </list>
        </property>
        <property name="fileEncoding" value="utf-8"/>
    </bean>
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
        <property name="properties" ref="configProperties" />
    </bean>


    <bean class="com.taobao.developtools.component.chiron.TailorBeanSpringProcessor">
        <property name="ignorePackagePrefixList">
            <list>
                <value>com.taobao.hsf.app.spring.util.HSFSpringProviderBean</value>
            </list>
        </property>
        <property name="needPackagePrefixList">
            <list>
                <value>org.apache.ibatis.session.SqlSessionFactory</value>
                <value>org.mybatis.spring.SqlSessionTemplate</value>
                <value>javax.sql.DataSource</value>
                <value>com.taobao.developtools</value>
                <value>com.tmall.geotherm.dal.cache</value>
                <value>com.tmall.geotherm.dal.common</value>
                <value>com.tmall.geotherm.dal.dao</value>
                <value>com.taobao.tair.impl.mc.MultiClusterTairManager</value>
            </list>
        </property>
    </bean>

    <import resource="classpath*:/spring-*****.xml" />
    <context:component-scan base-package="com.tmall.wireless.**.dal"/>
    <context:component-scan base-package="com.tmall.wireless.**.service"/>复制代码

其中LoadRequiredBeanSpringProcessor用来加载必须的bean及依赖树。配置needPackagePrefixList是配置必须加载的类的前缀(包括包路径),否则有的必须的类会被忽略掉。

LazyLoadBeanProcessor是用来懒加载所有bean。

  • mock示例

对于需要mock的服务(如远程方法等),可以通过@MockService跟@Service注解来实现,如果service注解指定名称,则的mock特定名称的服务,否则mock整个接口的所有实现的,如下图

优缺点

相比使用mock的方式,你依赖的BEAN都是真实的。不需要单独mock数据库,外围接口等麻烦的工作。但如果要mock,使用原有方案也不冲突。

但相对来说如果外围接口有问题,还是要自己mock的。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值