`

spring接口工程的Junit单元测试搭建

阅读更多

引言

 

前段时间对我们系统进行了微服务化拆分,最终出现几个单独的纯接口工程(没有web界面);最近又在搞一个基于国际化的纯接口转换工程。这些工程都有一个共同的特点,就是没有web界面,只是单纯的对外提供服务。没有界面,对应研发人员来说,很难进行自测。

 

以前我们研发的自测方式无非就两种:1、把接口工程部署到测试环境,在调用方的测试环境页面上进行测试;2、自己开发一个servlet的测试界面,进行测试。第一种方式,无法进行断点联调;第二种方式工作量大,需要为每个接口开发单独的页面,但这些页面对业务来说又没啥用。

 

基于以上原因,准备通过JUnit+spring bean的装配方式,搭建一个适用于纯接口工程的测试demo工程 给新同事作为参考。简单的说最终的效果就是要求不启动接口工程,采用非侵入的方式,就可以实现JUnit测试方法调用。这里所谓的非侵入,指的是不要影响业务代码。

 

本章所有示例代码,都已上传GitHub,地址详见文章结尾。

 

spring bean装配方式

 

由于我们的接口工程都是采用的spring作为Bean容器搭建的,要想使用Junit单元测试,就必须把所有相关的bean先进行装配,在测试方法调用前 被注入到spring容器。也就是说要为接口工程实现Junit单元测试,最主要的就是要自己实现bean的装配。这里有一定的工作量,所以很多系统都不愿去做。但如果设计得好,形成一个模式,就可以在各个接口工程中复用。

 

首先我们来分析下Spring提供的三种bean装配机制:



 

1、显式XML装配:在XML中进行显示的配置。这种方式一般是用在对jdbc连接池的配置,以及外部依赖接口的配置。还有一些老系统采用的老版本的spring,这些老系统基本都是采用的XMl配置的方式。

2、隐式自动装配:隐式的bean发现机制和自动装配。这是spring目前比较推崇的方式,目前对于我们内部能控制的业务bean都是采用的这种自动装配方式。但对于依赖参数或者外部bean,无法进行自动装配,我们系统一般采用的是第1XML配置方式。其实这种情况spring更推崇我们使用第3种方式。

3、显式java装配:在java中进行显示的配置。这种方式在我们系统中目前基本没有使用,但相比第1种方式会更加灵活,spring也推荐我们使用这种方式。

 

 

由于不同的适用场景,以及不同开发人员的习惯,我们的接口工程中可能同时存在这三类装配方式。我们的首要工作就是要在执行JUnit单元测试方法之前,把这些通过不同装配方式的bean自动注入到容器。下面分别对整合这三类方式进行讲解

 

整合“隐式自动装配”

 

Spring的隐式自动装配有两种形式:javaxmljava方式比较灵活可以分为三种,对应隐式自动装配的方式大致如下:



 

 

下面我们分别对每种方式的使用简单讲解,再运用到Junit单元测试创建中。

 

1、基于java注解@ComponentScan标记,标记在扫描类上(非业务类)。三种典型的使用方式:

a@ComponentScan不带参数:会扫描该被标记类根目录、以及所有子目录下bean类,并把扫描到的所有包含@Component标记的bean类自动装配并注入容器。这种方式侵入性,会在业务代码目录下创建一个扫描类,尽量避免使用,如下的UserServiceConfig类,只是为了扫描使用,类体为空:

@ComponentScan
public class UserServiceConfig {
   
}

所在目录为:



 

 

所在的目录中UserService为接口类跳过,子目录中UserServiceImpl类被Component标记,会被扫描到,进行自动装配,UserServiceImpl代码如下:

@Component("us")
public class UserServiceImpl implements UserService {
 
    @Autowired(required = false)
    private UserDao userDao;
 
    @Override
    public void add() {
        if (userDao != null){
            userDao.add();
        }
        System.out.println("service层:用户添加成功");
    }
}

 

 

test包(不会被maven打入部署包中)创建Junit测试代码类UserServiceTest,代码如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=UserServiceConfig.class)
public class UserServiceTest {
    @Autowired
    private UserService us;
 
    @Test
    public void usNullTest(){
        Assert.notNull(us);
        us.add();
    }
}
 

 

@RunWith(SpringJUnit4ClassRunner.class),指定使用Junit4spring一起使用。

@ContextConfiguration(classes=UserServiceConfig.class),指定spring自动装配路径为UserServiceConfig的跟目录,及其子目录。

执行JunitusNullTest方法,打印信息为:

 

service层:用户添加成功

 

说明UserServiceImpl自动装配成功,但是它依赖的UserDao没有被注入。

类似的我们可以在UserDao所在的根目录创建一个UserDaoConfig,并标记为@ComponentScan

@ContextConfiguration(classes={UserServiceConfig.class,UserDaoConfig.class})添加到junit测试类中:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={UserServiceConfig.class,UserDaoConfig.class})
public class UserServiceTest {
    @Autowired
    private UserService us;
 
    @Test
    public void usNullTest(){
        Assert.notNull(us);
        us.add();
    }
}

 

 

再次执行usNullTest测试方法,打印信息为:

dao层:用户添加成功
service层:用户添加成功

 

说明UserDao被自动装配到UserServiceImpl中,并注入到容器。该方法的Junit单元测试方法创建并测试通过,并无需部署和启动程序就可以完成测试。这里的add测试方法比较简单,正常的业务,可以还需手动传入各种不同的参数,对该方法进行测试。

 

但这种方法有个弊端,就是需要在每个业务bean跟目录下去创建一个配置扫描类,对业务有侵入性,而且创建配置扫描类多个也非常麻烦。在创建Junit单元测试时,你可以在test包中创建一个跟业务包相同的包路径,并把扫描类放到该路径下,可以减少侵入性,比如上述UserServiceConfig扫描类可以这样创建:

 

 


 

但各个子工程模块test包中的代码是彼此不可见的,所有还是有一定局限。

 

b@ComponentScan(basePackages = {"com.xx1","com.xx2","com.xx3”}) basePackages参数,采用这种配置方式,可以完全做到非侵入式:扫描类可以创建在test包中,由Packages指定需要扫描的路径。这是我个人非常建议的方式,具体操作只需要在test包中创建一个PackageScaner类(业务代码包中不会再有扫描类),代码如下:

@Configuration
@ComponentScan("com.sky.locale")
public class PackageAllScaner {
}

 

再创建一个全面的Junit测试类(不建议一个系统就建一个测试类,实际根据具体业务进行拆分)AllAutoServiceTest,代码如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=PackageAllScaner.class)
public class AllAutoServiceTest {
 
    @Autowired
    private UserService us;
 
    @Autowired
    private ProductService ps;
 
    @Autowired
    private OrderService os;
 
    @Test
    public void usNullTest(){
        Assert.notNull(us);
        us.add();
 
        Assert.notNull(ps);
        ps.add();
 
        Assert.notNull(os);
        os.add();
    }
 
}

 

 

运行测试方法,打印结果如下:

dao层:用户添加成功
service层:用户添加成功
dao层:商品添加成功
service层:商品添加成功
dao层:订单添加成功
service层:订单添加成功

 

说明所有包下的自动装配都注入成功并测试通过,实际开发中不建议把多个测试写在一个测试方法,根据具体业务调整。

 

C@ComponentScan(basePackageClasses = {xxx1.class, xxx2.class })指定basePackageClasses参数:这种方式可以扫描指定xxx1xxx2类所在的目录及其子目录下 @Component标记bean,并进行自动装配。相比第二种方式,该扫描类也可以创建在test包中,在一定程度上没有侵入性。但如果需要扫描的目录下没有类,就需要自己创建一个空类作为基准,个人不是很推荐。如果一定要创建:可以在test包下创建以一个跟业务包完全相同的路径,并在该路径下创建扫描类。具体使用方式:

扫描类:

@ComponentScan(basePackageClasses = {ServiceScan.class})

public class ClassScaner {

}

 

ServiceScan类在test包下,上述代码会扫描业务包com.sky.locle.service所有带@Component标记的bean




Junit测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=ClassScaner.class)
public class UserServiceTest {
    //这里就不写测试方法了,感兴趣的可以自己尝试下
}

 

如果待扫描的目录下存在业务类,可以使用,否则需要自己创建一个空的扫描类,具有侵入性,这时不建议使用。

 

“隐式自动装配”基于java@ComponentScan标记 的三种方式就将完了。总结下,我们做非侵入的Junit单元测试,最好选择第二种指定package方式@ComponentScan(basePackages = {"com.xx1","com.xx2","com.xx3”})

 

2XMl配置:基于XML配置实现的“隐式自动装配”配置方式为:<context:component-scan base-package="com.xxx" />。在实际开发中,我们经常使用的方式,效果等同于java注解的第二种方式:@ComponentScan(basePackages = {"com.xxx"})

 

我们在写Junit单元测试时,不需要创建自己的xml配置文件,如果一定要创建可以在test包下,防止侵入业务代码。但我们可以在Junit测试代码中直接引用已有的xml配置文件。

假如,业务代码中已存在一个xml bean配置文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                            http://www.springframework.org/schema/context/spring-context.xsd">
 
    <context:component-scan base-package="com.sky.locale.service.product" />
    <context:component-scan base-package="com.sky.locale.dao.product" />
 
</beans>

我们Junit测试类可以直接引入使用:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring-test.xml"})
public class XmlAutoServiceTest {
    @Autowired
    private ProductService ps;
 
    @Test
    public void usNullTest(){
        Assert.notNull(ps);
        ps.add();
    }
}

执行测试方法,打印信息为:

dao层:商品添加成功
service层:商品添加成功

测试通过,基于“隐式自动装配”这种方式的 Junit单元测试 创建方式 讲解结束。

 

整合显式XML装配

 

显式XML装配 spring 2.5以前版本里大量使用。现在一些无法自动装配的bean也会选择使用这种方式,比如配置jdbc连接池以及外部依赖的接口。我们现在常用jdbc配置方式是,把jdbc参数信息放到.properties配置文件中,然后通过XMl装配的方式注入到容器中。

 

首先我们先看下常用的XML装配方式:

1、不带id的方式:

<!-- 不指定id,默认id为:com.sky.locale.service.explicit.impl.ExplicitTestServiceImpl#0 -->
       <bean class="com.sky.locale.dao.explicit.impl.ExplicitTestDaoImpl" />

2、带id方式:

<bean id="explicitTestDao" class="com.sky.locale.dao.explicit.impl.ExplicitTestDaoImpl" />

3、构造方法方式,成员为引用,使用ref;成员为基础类型,使用value

<bean id="explicitTestService" class="com.sky.locale.service.explicit.impl.ExplicitTestServiceImpl">
              <constructor-arg name="explicitTestDao" ref="explicitTestDao"/>
       </bean>
 

 

4、构造方法 c命令空间方式,构造方法方式的简化版:

<bean id="explicitTestService1" class="com.sky.locale.service.explicit.impl.ExplicitTestServiceImpl"
             c:explicitTestDao-ref="explicitTestDao" />
 

 

5settter方式:

<!-- setter注入-->
       <bean id="explicitTestService2" class="com.sky.locale.service.explicit.impl.ExplicitTestServiceImpl">
              <property name="explicitTestDao" ref="explicitTestDao"/>
              <property name="name" value="123"/>
              <property name="books">
                     <list>
                            <value>monkeys</value>
                            <value>pigs</value>
                     </list>
              </property>
       </bean>

6setter对应的p命名空间方式:

<!-- p 命名空间注入 -->
       <bean id="explicitTestService3" class="com.sky.locale.service.explicit.impl.ExplicitTestServiceImpl"
               p:explicitTestDao-ref="explicitTestDao"
               p:name="123">
              <property name="books">
                     <list>
                            <value>monkeys</value>
                            <value>pigs</value>
                     </list>
              </property>
 
       </bean>

对于这种listset等集合成员,还可以单独提取出来,变形成这样:

<!-- 使用util:list把list转移出去 -->
       <bean id="explicitTestService5" class="com.sky.locale.service.explicit.impl.ExplicitTestServiceImpl"
             p:explicitTestDao-ref="explicitTestDao"
             p:name="123"
             p:books-ref="books">
       </bean>
 
       <util:list id="books">
              <value>monkeys</value>
              <value>pigs</value>
       </util:list>

这种提取,同样适用于c命令空间。

 

对于这种方式的整合到Junit,其实前面已经用到,直接在Junit测试类中指定对应的xml即可:@ContextConfiguration(locations = {"classpath:xxxx.xml"})。这里我们以Junit整合一个jdbc数据源为例进行讲解,首先看下需要装配的数据源类TestJdbcSource

public class TestJdbcSource {
 
    private String userName;
 
    private String password;
 
    private String uri;
 
    public TestJdbcSource(String userName,String password,String uri){
        this.userName = userName;
        this.password = password;
        this.uri = uri;
    }
 
    public void getSource(){
        System.out.println("连接数据库uri:"+uri);
        System.out.println("连接数据库用户名:"+userName);
        System.out.println("连接数据库密码:"+password);
    }
}

 

 

然后看下装配该beanxml配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <!-- 不需要依赖的id,可以不用指定-->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:test.properties</value>
            </list>
        </property>
    </bean>
 
    <bean id="testDbSource" class="com.sky.locale.dao.jdbc.TestJdbcSource">
        <constructor-arg name="uri" value="${jdbc.url}"/>
        <constructor-arg name="userName" value="${jdbc.username}"/>
        <constructor-arg name="password" value="${jdbc.password}"/>
    </bean>
</beans>

在看下属性配置文件test.properties内容:

jdbc.url=jdbc:msyql:loadbalance://localhost/test_db
jdbc.username=root
jdbc.password=123
 

 

我们要在JUnit中使用该数据源,可以直接引入xml配置即可,代码如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring-jdbc.xml"})
public class JdbcSourceTest {
 
    @Autowired
    private TestJdbcSource testDbSource;
 
    @Test
    public void jdbcTest(){
        Assert.notNull(testDbSource);
        testDbSource.getSource();
    }
}

运行测试方法,打印信息为:

连接数据库uri:jdbc:msyql:loadbalance://localhost/test_db
连接数据库用户名:root
连接数据库密码:123

说明Junit整合显示XML装配成功。

 

整合显式java装配

 

这种方式类似显示xml装配,但比xml更加灵活。新版spring也推荐使用这种方式(所谓的去xml化),但可惜的是现在我们系统还基本没有使用过。我们首先看下显式java装配方法有那些形式:

 

首先创建一个空类,@Configuration表示该类为一个配置类:

@Configuration
public class ExplicitTestConfig {
}
 

 

然后依次往里面添加下列方法,进行显示java装配

1、没有依赖其他对象的bean,直接调用其无参构造方法:

@Bean(name="explicitTestDao")
    public ExplicitTestDao explicitTestDao(){
        return new ExplicitTestDaoImpl();
    }
 

 

2、有依赖其他对象的bean,调用其构造方法,并注入需要的对象:

@Bean(name = "explicitTestService0")
    public ExplicitTestService explicitTestService0(){
        return new ExplicitTestServiceImpl(explicitTestDao());
}

 

这里以explicitTestDao()方法,会让人错误的以为多次的创建了新的对象,其实spring默认是单例,这种写法不会重复创建对象。

3、有依赖其他对象的bean,为避免第2中方式的错觉,可以直接以bean名注入:

@Bean(name = "explicitTestService1")
    public ExplicitTestService explicitTestService1(ExplicitTestDao explicitTestDao){
        return new ExplicitTestServiceImpl(explicitTestDao);
}

 

推荐使用这种方式,同时注意这里创建的bean2中相同,但name不同。

 

4setter方式注入,与方式2对应:

@Bean(name = "explicitTestService2")
    public ExplicitTestService explicitTestService3(){
        ExplicitTestServiceImpl impl = new ExplicitTestServiceImpl();
        impl.setExplicitTestDao(explicitTestDao());
        return impl;
}

5setter方式注入,与方式3对应:

@Bean(name = "explicitTestService3")
    public ExplicitTestService explicitTestService2(ExplicitTestDao explicitTestDao){
        ExplicitTestServiceImpl impl = new ExplicitTestServiceImpl();
        impl.setExplicitTestDao(explicitTestDao);
        return impl;
}
 

 

可以看到通过显示java装配的方式,可以在方法里任意的实现自己的逻辑,比”xml”更加灵活。

 

Junit整合,也很简单,直接在JUnit测试类中通过注解引入上述配置类,@ContextConfiguration(classes=ExplicitTestConfig.class)即可。

 

整合三种装配方式

 

前面把Junit分别三种装配方式整合进行了讲解。但我们所在的项目,业务代码很有可能以上三种装配方式都有使用,这时候创建Junit单元测试,需要把不同的装配方式整合到一起。其中自动装配其实也是通过,java或者xml配置实现的。所有我们只需要整合java装配和xml装配。具体可以使用@import @importResource注解来实现

 

假设有个业务测试,需要用到商品接口、用户接口、jdbc数据连接。我们可以把各个业务块的java装配、xml转配整合到一起。具体整合内容如下:

 
@Import({UserServiceConfig.class,ProductServiceConfig.class})
@ImportResource("classpath:spring-jdbc.xml")
public class MultConfig {
}
 
@ComponentScan
public class UserServiceConfig {
}
 
@ComponentScan
public class ProductServiceConfig {
 
}
 
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <!-- 不需要依赖的id,可以不用指定-->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:test.properties</value>
            </list>
        </property>
    </bean>
 
    <bean id="testDbSource" class="com.sky.locale.dao.jdbc.TestJdbcSource">
        <constructor-arg name="uri" value="${jdbc.url}"/>
        <constructor-arg name="userName" value="${jdbc.username}"/>
        <constructor-arg name="password" value="${jdbc.password}"/>
    </bean>
</beans>

 

 

这种整合方式有点类似 我们用一个顶级的xml配置文件,整合各个业务模块xml配置一样:

<?xml version="1.0" encoding="GBK"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                     http://www.springframework.org/schema/beans/spring-beans.xsd"
       default-autowire="byName">
 
    <import resource="xx1.xml" />
    <import resource="xx2.xml" />
    <import resource="xx3.xml" />
</beans>

 

整合完成后,创建Junit测试类,这时只需引入整合后的MultConfig类即可

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=MultConfig.class)
public class MultTest {
    @Autowired
    private UserService us;
 
    @Autowired
    private ProductService ps;
 
    @Autowired
    private TestJdbcSource testDbSource;
 
    @Test
    public void mutliTest(){
        Assert.notNull(us);
        Assert.notNull(ps);
        Assert.notNull(testDbSource);
        us.add();
        ps.add();
        testDbSource.getSource();
    }
}

 

 

执行测试方法,打印结果为:

service层:用户添加成功
service层:商品添加成功
连接数据库uri:jdbc:msyql:loadbalance://localhost/test_db
连接数据库用户名:root
连接数据库密码:123

整合成功。

理论上通过这种方式,你可以把程序中所有bean都注入到容器中,而不需要部署程序,就可以对任意一个接口方法进行测试。

 

通过以上讲解,应该覆盖了我们在创建Junit单元测试所遇到的大部分情况。根据不同场景,灵活运用,可以让我们的纯接口工程测试更加轻松。

 

以上示例代码已上传到GitHub,地址:https://github.com/gantianxing/spring-test.git

 

 

 

  • 大小: 18.3 KB
  • 大小: 32.5 KB
  • 大小: 7.7 KB
  • 大小: 18.4 KB
  • 大小: 9.3 KB
1
1
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics