基于Spring的单元测试

       企业级Java程序基本都离不开SpringSpringBoot,它们是目前企业级Java事实上的标准。我们的代码中或多或少都会使用到 @Autowired注解来引入依赖,或者在类型上声明 @Compoenent注解来将类型实例托管到(Spring)容器中,这就带来了一个问题,如果没有Spring,谁来帮我们组装类之间的依赖关系?

       在前文中可以看到MemberServiceImpl依赖了UserDAO,通过调用setUserDAO方法,可以将MemberServiceImpl依赖的实例设置给它,可是如果需要测试的类型有很多依赖怎么办呢?还需要一一调用set方法设置吗?

       答案是否定的。Spring从诞生的开始,就考虑到如何在Spring环境下做单元测试,而Spring-Test就是Spring提供的单测套件。Spring-Testspringframework中一个模块,也是由spring作者Juergen Hoeller亲自操刀设计开发的,它可以方便的测试基于spring的代码。

使用Spring-Test来进行单元测试

       Spring-Test是基于JUnit的单测套件,由于测试会启动spring容器,所以需要依赖Spring配置,同时要继承Spring-Test提供的超类。在使用Spring-Test前,首先要进行依赖配置,依赖的maven坐标如下:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

       MemberServiceImpl依赖UserDAO,而Mockito.mock(Class clazz)方法可以创建一个clazz类型的Mock对象,在spring体系中,Mockito就如同一个FactoryBean,因此我们可以通过一段spring配置,将MemberServiceImpl装配起来,对应的spring配置如下所示:

<?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"
       default-autowire="byName">

    <bean id="memberService" class="com.murdock.tools.mockito.service.MemberServiceImpl"/>

    <bean id="userDAO" class="org.mockito.Mockito" factory-method="mock">
        <constructor-arg value="com.murdock.tools.mockito.dao.UserDAO"/>
    </bean>
</beans>

       如上述配置所示,熟悉spring的同学一定会觉得很亲切,MemberServiceImpl被声明为id为memberService的bean,而userDAO对应的bean是由Mockito创建出来的Mock对象。接下来,只需要使用Spring-Test提供的超类,就可以编写基于Spring-Test的测试了。

       MemberService的单测(部分代码)如下所示:

@ContextConfiguration(locations = {"classpath:MemberService.xml"})
public class MemberSpringTest extends AbstractJUnit4SpringContextTests {
    @Autowired
    private MemberService memberService;
    @Autowired
    private UserDAO userDAO;

    /**
     * 可以选择在测试开始的时候来进行mock的逻辑编写
     */
    @Before
    public void mockUserDAO() {
        Mockito.when(userDAO.insertMember(Mockito.any())).thenReturn(
                System.currentTimeMillis());
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_member_error() {
        memberService.insertMember(null, "123");

        memberService.insertMember(null, null);
    }

    /**
     * 也可以选择在方法中进行mock
     */
    @Test(expected = IllegalArgumentException.class)
    public void insert_exist_member() {
        MemberDO member = new MemberDO();
        member.setName("weipeng");
        member.setPassword("123456abcd");
        Mockito.when(userDAO.findMember("weipeng")).thenReturn(member);

        memberService.insertMember("weipeng", "1234abc");
    }
}

       从上述测试代码可以看出,Spring-Test会要求测试类型继承AbstractJUnit4SpringContextTests,同时使用注解@ContextConfiguration指定当前测试需要使用的Spring配置。我们编写的单元测试肯定不止一个,当编写另一个单元测试类时,就有同学会想着用已有的配置文件,这么做,好吗?可以看到示例中,配置专门放在MemberService.xml中,没有使用共用的配置文件,目的就是让大家在测试的时候能够相互独立,避免搞坏别人的测试,让不同的测试相互之间不影响。并且在一个配置文件中配置的Bean越多,就证明你要测试的类依赖越复杂,承担的责任过多,从而提醒自己做重构。

现代化的Spring-Test使用方式

       Spring从4.0后,就开始推荐使用Java配置方式了,也就是大家常用的@Configuration和@Bean,通过编写Java配置类来装配Spring的Bean。现阶段大家使用的Spring都比较新,因此我们可以将以传统形式配置的Spring-Test单测改造为现代化的配置方式。

       使用Java配置方式的MemberService单测代码如下所示:

       可以看到基于Java配置方式的单测比传统基于xml的Spring-Test要显得内聚很多,没有了测试配置文件,只有一个单元测试类,Spring的配置和测试类也可以是一体的。通过@Configuration修饰的MemberServiceConfig可以被配置在@ContextConfiguration中,被用来装配一个测试的Spring容器。

       测试类也不需要继承AbstractJUnit4SpringContextTests,只需要使用@RunWith注解修饰即可。不需要xml配置,配置和测试是一体的,现代化的Spring-Test变得更好了,和配置文件一样,当配置类中的@Bean变多时,就要反思实现类是否职责过多或者依赖过重了。

SpringBoot环境下的测试方法

       Spring框架诞生后,成为了企业级Java(与互联网Powered by Java)的事实标准,IoC无往不利,势如破竹,从代码组成和思维方式上改变了整个Java开发生态。在2.5.6发布后,Spring框架就进入了一个低潮期,Spring不愿意触及部署与实现,这种不愿下场的做法使得其多年没有任何实质变化,被淘汰只是时间问题,但是这时候Phil Webb等人发起的SpringBoot项目挽狂澜于既倒,点燃了Spring的第二春。

       SpringBoot提供了开箱即用的starter体系,通过自动装配能够提供给应用更便捷的bean配置方式,同时它支持迅速打包为jar-in-jar的形式,并通过java -jar的形式进行运行,改变了企业级Java使用容器部署的主流方式。在改变部署形态的同时,提供了多环境和配置的解决方案,在微服务和容器化崛起的时候,顺势而为,一举成为企业级Java部署的现实标准。

       可以看到相比Spring框架,SpringBoot会载入更多的Bean,同时由于测试类可能依赖了starter,就需要在单测执行前完成starter的配置,因此SpringBoot也提供了相应的测试套件,即Spring-Boot-Test

Spring-Boot-Test 对Mockito进行了整合,在进行Mock时,相比Spring-Test会方便一些。

       接下来,让我们用Spring-Boot-Test改造一下MemberService的单测,改造后的测试代码如下:

@SpringBootTest(classes = SpringBootMemberTest.Config.class)
@TestPropertySource(locations = "classpath:test-application.properties")
@RunWith(SpringRunner.class)
public class SpringBootMemberTest {

    @Autowired
    private Environment env;
    @MockBean
    private UserDAO userDAO;
    @Autowired
    private MemberService memberService;

    @Before
    public void init() {
        Mockito.when(userDAO.insertMember(Mockito.any())).thenReturn(System.currentTimeMillis());
    }

    @Test
    public void insert_member() {
        System.out.println(memberService.insertMember("windowsxp", "abc123"));
        Assert.assertNotNull(memberService.insertMember("windowsxp", "abc123"));
    }

    @Configuration
    static class Config {

        @Bean
        public MemberService memberService() {
            return new MemberServiceImpl();
        }
    }

}

       如上述代码所示,基于Spring-Boot-Test的单测需要使用注解@SpringBootTest标注,声明该类为一个SpringBoot测试类,同时与Spring-Test类似,通过classes属性声明当前测试类使用的配置,本示例中是SpringBootMemberTest.Config

       对于需要Mock的类型,可以使用@MockBean注解来修饰,它会生成对应类型的Mock对象,并将其注入到容器中。当然SpringBoot离不开application配置,可以通过@TestPropertySource注解指定当前测试用例所使用的application配置。

       如果测试的类需要依赖一些starter才能工作,那就需要在测试类上增加@EnableAutoConfiguration,同时在application配置中增加一些属性,这样该测试就会像一个SpringApplication一样被启动起来。

SpringBoot环境下持久层测试

       持久层的单测很重要,是应用单测的基础,而且由于持久层的单测一般不会选择mock数据源,因此测试过程除了正确性的保证之外,还需要确保测试过程对数据库中的数据不会产生影响。

       还是通过UserDAO的单测来演示SpringBoot的持久层单测编写,由于测试需要依赖数据库,因此在示例中需要先使用StartDB启动一个hsqldb,然后再运行单测UserDAOImplTest。hsqldb是一款Java编写的嵌入式数据库,可以使用内存或主机模式启动,本示例中采用后者,以独立进程的方式启动,在数据库启动后,接下来看一下对应的测试,代码如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = UserDAOImplTest.Config.class)
@TestPropertySource(locations = "classpath:test-application.properties")
@EnableAutoConfiguration
public class UserDAOImplTest extends AbstractTransactionalJUnit4SpringContextTests {
    @Autowired
    private UserDAO userDAO;
    @Test
    public void findMember() {
        MemberDO member = new MemberDO();
        member.setId(1L);
        member.setName("name");
        member.setPassword("password");
        member.setGmtCreate(new Date());
        member.setGmtModified(new Date());
        userDAO.insertMember(member);
        MemberDO name = userDAO.findMember("name");
        Assert.assertNotNull(name);
        Assert.assertEquals("password", name.getPassword());
    }
    @Import(MyBatisConfig.class)
    @Configuration
    static class Config {
        @Bean
        UserDAO userDAO() {
            return new UserDAOImpl();
        }
    }
}

       可以看到,测试类需要继承AbstractTransactionalJUnit4SpringContextTests,这样任意测试方法都会进行回滚,避免对数据造成实际的影响。我们运行一个持久层的测试,不希望它更改已有的测试数据,也希望测试方法之间相互不存在影响,而该超类能够让所有测试方法的执行最终都会进行回滚,从而避免对数据库中的数据产生副作用。

       测试方法需要进行数据准备,然后进行查询验证。除了验证数据是否为空,最好能够抽检几个字段,看看是否符合预期。

results matching ""

    No results matching ""