NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

了解Spring Test对单元测试和集成测试的支持

| Comments

在敏捷开发和测试驱动开发中,自动化测试一直被认为是让软件开发人员能够有信心的进行软件开发的力量源泉。

所以,软件开发人员不仅要关注写出高质量的产品代码,同时也要关注写出高质量的测试代码。

有些人说,对于开发人员,自动化测试不过是利用Junit,Mockito,Selenium等测试框架写出的另一段程序代码而已。虽然听起来很不爽,但我也不否认这一点,这是事实,但我们也是按照写产品代码的要求来写出高质量的测试代码。

回到正题,今天的主题是了解Spring对基于Spring开发的单元测试和集成测试的支持。

如果你正在使用Spring做开发,那么在写测试代码的时候,无论是单元测试还是集成测试,如果只是用到Junit,Mockito,EasyMock这些测试框架,你一定会发现,它们是不够的。Spring框架的优点就在于,它的策略是试图涵盖Java开发的所有部分,测试也不例外。Spring提供了一套API来支持基于Spring的单元测试和集成测试。下面,我们来看看Spring在测试方面都提供一些什么样的优秀特性。

反射测试工具 org.springframework.test.util.ReflectionTestUtils

依赖注入是Spring框架提供的最主要的特性之一,在Spring上下文管理范围内,Spring提供三种注入方式,Field注入,构造器注入,Setter注入。我们都知道在写单元测试时,只会关注被测试类本身的逻辑,一般我们都会将类中的依赖进行mock。

在没有Spring提供的反射测试工具的时候,我们一般都倾向于构造器注入,Setter注入的方式,因为这样在写测试代码时候,可以将mock的依赖传入到被测试类。但实际上,在Field上注入更符合Spring风格,或者更容易理解,我需要Spring给我注入这个对象,我就在它上面加一个@Autowired注解。所以构造器注入和Setter注入多少都是为了方便测试,不得已而为之。更有甚者,其实采用的是Field注入,但是为了测试,不得以添加一个setter方法。这就违反了简洁代码的原则,无用的方法,或者只被测试用到方法。

还好,Spring提供的ReflectionTestUtils拯救了我们。它提供这样的一个方法,ReflectionTestUtils.setField(),通过反射的方式将想要的依赖设置到对应的field上。

1
2
3
4
5
@param target the target object on which to set the field
@param name the name of the field to set
@param value the value to set

ReflectionTestUtils.setField(xxService, "xxDao", mockedXxDao.class);

Spring MVC测试的Mock对象:MockHttpServletRequest,MockHttpSession等。

如果你正在测试Controller,而Controller又恰好对HttpServletRequest和HttpSession有操作,那么Spring提供的MockHttpServletRequest,MockHttpSession就派上用场了。

1
2
3
4
MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
MockHttpSession mockHttpSession = new MockHttpSession();
mockHttpSession.setAttribute("username", "user");
mockHttpServletRequest.setSession(mockHttpSession);

当然,针对上面这种做法,你采用mockito也是可以实现的,而且代码量也差不多。

1
2
3
4
HttpServletRequest mockHttpServletRequest = mock(HttpServletRequest.class);
HttpSession mockHttpSession = mock(HttpSession.class);
when(mockHttpSession.getAttribute("username")).thenReturn("user");
when(mockHttpServletRequest.getSession()).thenReturn(mockHttpSession);

但是,如果你的Controller类中,使用了某个静态工具类方法,它对HttpServletRequest和HttpSession做了各种各样的操作,来维护当前用户的某些状态。从本质上,你应该将静态方法mock,但这并不容易实现。于是,你就必须mock HttpServletRequest和HttpSession中各种各样的依赖,来保证静态工具类方法不会抛空指针,而其中这些会抛空指针的操作与你期待的测试的行为无关。最直接的结果就是测试中的mock逻辑会特别的多,关键还不一定正确。

Spring的MockHttpServletRequest和MockHttpSession就可以解决这类问题,它默认初始化好大部分HttpServletRequest和HttpSession需要的依赖,所以不会出现空指针问题,也就简化了mock的过程,而其他你期待的返回结果都可以通过setter配置进去。

Spring在集成测试方面的支持

SpringJUnit4ClassRunner,@ContextConfiguration,@WebAppConfiguration,MockMvc

SpringJUnit4ClassRunner:它是在Junit下启动Spring集成测试的基础,为Junit提供Spring TestContext Framework所拥有的功能。
@ContextConfiguration:用来决定根据什么样的配置为集成测试加载和配置ApplicationContext。
@WebAppConfiguration:用来声明集成测试加载的ApplicationContext应该是一个WebApplicationContext。
MockMvc:是Spring在服务器端对Spring MVC测试的支持,可以在对Controller的集成测试中,模拟对Controller某个Request的调用,是非常实用的集成测试组件。

看下面一个例子,如何使用上面4个组件来实现Controller的集成测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import ...

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = WebContextConfiguration.class)
@WebAppConfiguration
public class xxxIntegrationTest {

  private MockMvc mockMvc;

  @Autowired
  private WebApplicationContext webApplicationContext;

  @Before
  public void setUp() throws Exception {
      mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
  }

  @Test
  public void shouldReturnKits() throws Exception {
      mockMvc.perform(get("/form"))
          .andExpect(status().isOk())
          .andExpect(content().mimeType("text/html"))
          .andExpect(forwardedUrl("/WEB-INF/layouts/main.jsp"));
  }
}

测试类的事务管理 @TransactionConfiguration,@Transactional,@BeforeTransaction,@AfterTransaction

如果你正在对Dao层或者说数据库做集成测试,包括了CRUD所有基本操作的测试,那么你肯定会希望在单个测试运行结束之后,可以将数据库的状态回滚,这样除了可以重复的运行测试,更重要的是不会影响到其他测试,不会弄脏数据。那么,Spring提供的事务管理功能,除了可以实现产品代码中的事务管理,还可以实现测试代码的事务管理。

@TransactionConfiguration是为集成测试提供的类似@EnableTransactionManagement的功能的注解,用来显示的为集成测试指定某个TransactionManager和Rollback策略。但它并不是必须的,如果在Spring的上下文中,只有一个TransactionManager,且bean的名字是transactionManager,并且你认为的默认策略是Rollback,那么就可以不必配置@TransactionConfiguration。

@Transactional就和产品代码中一样,你需要某个测试类具有事务功能,就在该测试类上加上@Transactional注解。

1
2
3
4
5
6
7
import ...

@TransactionConfiguration(transactionManager = "txManager")
@Transactional
public class xxxDaoIntegrationTest {
...
}

@BeforeTransaction,@AfterTransaction,顾名思义,就是在事务前和事务后做的对应操作。

@TestPropertySource,@DirtiesContext

@PropertySource用来以声明式的方式将Properties加载到Spring的Environment变量中,@TestPropertySource拥有比@PropertySource更高的优先级,可以用来加载专门为测试提供的Properties。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
     @Autowired
     Environment env;

     @Bean
     public TestBean testBean() {
         TestBean testBean = new TestBean();
         testBean.setName(env.getProperty("testbean.name"));
         return testBean;
     }
}
1
2
3
4
5
@ContextConfiguration
@TestPropertySource("/test.properties")
public class MyIntegrationTests {
    // class body...
}

@DirtiesContext用来说明在某个测试执行之后,会导致Spring Context被污染,比如,修改了某个执行策略,改变了某个单例对象的状态。此时,该Context应该被关闭,之后的测试会使用新的Context。

@DirtiesContext可以被用在测试类上,也可以用在测试方法上,具体行为如下:
* after the current test, when declared at the method level
* after each test method in the current test class, when declared at the class level with class mode set to AFTER_EACH_TEST_METHOD
* after the current test class, when declared at the class level with class mode set to AFTER_CLASS

这里只是介绍一些常用的比较重要的特性,除了以上这些,Spring Test Framework还提供许多其他特性,有效利用它们,可以让你写出方便的进行基于Spring的应用的测试,同时也是保证写出高质量测试的基础。

参考资料:
1.Spring Reference Document, Spring Test

了解Spring Transaction事务管理

| Comments

提供一种统一、抽象的编程模型来管理不同的事务API,如,JavaTransactionAPI(JTA),JDBC,Hibernate,Java Persistence API (JPA)以及Java Data Objects (JDO),是选择Spring Transaction做事务管理最直接的理由。

本地事务和全局事务

全局事务让你可以和多个事务资源工作在一起,比如,关系型数据库,消息队列。

而本地事务则是与某个指定的事务资源联系在一起,比如,与JDBC连接相关的事务。本地事务相对于全局事务更容易使用,但不能跨多个事务资源。管理JDBC连接所写的事务代码不能够在全局事务中使用。

Spring Transaction

Spring Transaction使得开发人员在任何一个环境中都可以使用相同的编程模型。只要写一次代码,就可以在不同环境下的不同的事务策略中使用。最重要的是Spring提供了声明式的事务管理方式,可以通过配置的方式实现事务管理。

Spring事务抽象中一种关键的概念是:事务策略。

一个事务策略,由PlatformTransactionManager接口所定义:

1
2
3
4
5
6
7
8
public interface PlatformTransactionManager {

  TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

  void commit(TransactionStatus status) throws TransactionException;

  void rollback(TransactionStatus status) throws TransactionException;
}

PlatformTransactionManager是事务管理的抽象层,Spring根据这个抽象层提供许多不同的具体实现。无论是声明式还是编程式的进行事务管理,你都必须正确的定义PlatformTransactionManager的实现。

在使用PlatformTransactionManager的具体实现时,通常都需要一些与对应工作环境的相关知识,比如:JDBC,JTA,Hibernate。

下面是一个JDBC的例子:

1
2
3
4
5
6
7
8
9
10
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
</bean>

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
</bean>

下面是一个Hibernate的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="mappingResources">
            <list>
                <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <value>
                hibernate.dialect=${hibernate.dialect}
            </value>
        </property>
</bean>

<bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
</bean>

声明式的事务管理

大部分Spring框架的使用者都会采用声明式的事务管理,因为这样做对产品代码的侵入性是最低的。这种声明式的事务管理,使得它可以和Spring的切面编程(AOP)结合在一起,不过即便你不了解AOP,仍然可以使用,因为它几乎是模板式的配置。

这种声明式的事务管理可以允许你在方法级别上指定事务行为。

理解Spring声明式事务的实现

要理解Spring声明式事务的实现,你需要知道的最重要的概念就是,Spring声明式事务的实现是通过Spring的AOP代理。

下面是一个通过XML方式配置事务的例子,方便你理解事务的实现方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
    <tx:advice id="txAdvice" transaction-manager="txManager"> <!-- the transactional semantics... -->
        <tx:attributes>
            <!-- all methods starting with 'get' are read-only -->
            <tx:method name="get*" read-only="true"/>
            <!-- other methods use the default transaction settings (see below) -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
    <!-- ensure that the above transactional advice runs for any execution
        of an operation defined by the FooService interface -->
    <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </aop:config>
    <!-- don't forget the DataSource -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>
    <!-- similarly, don't forget the PlatformTransactionManager -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- other <bean/> definitions here -->
</beans>

对于x.y.service.FooService包中的任何一个服务类的任何一个方法,我都需要它被txManager所管理,任何以get开头的方法都让它在只读事务的上下文中执行,其他的则在默认事务的上下文中执行。

Note:如果DataSourceTransactionManager的bean name,定义为transactionManager,则<tx:advice>中的transaction-manager不用指定。

注解的方式

在真正的开发当中,除了这种xml的方式来指定事务的配置,通过注解的方式来配置事务相对更简单一些。

1
2
3
4
5
6
7
8
9
10
@Transactional
public class DefaultFooService implements FooService {
  Foo getFoo(String fooName);

  Foo getFoo(String fooName, String barName);

  void insertFoo(Foo foo);

  void updateFoo(Foo foo);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <!-- enable the configuration of transactional behavior based on annotations -->
    <tx:annotation-driven transaction-manager="txManager"/>
    <!-- a PlatformTransactionManager is still
     required -->
    <bean id="txManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- (this dependency is defined somewhere else) -->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- other <bean/> definitions here -->
</beans>

Note:同样的道理,如果DataSourceTransactionManager的bean name,定义为transactionManager,则<tx:annotation-driven>中的transaction-manager不用指定。

要真正启动事务管理,仅仅配置@Transactional是不够的,一定要配置<tx:annotation-driven>来启动事务管理行为。

Note:如果你使用Java的配置方式,只需要在@Configuration的类上添加@EnableTransactionManagement来启动事务管理。

Note:@EnableTransactionManagement和<tx:annotation-driven/>在查找@Transactional时,只会在它们定义位置的上下文中查找。意味着,如果你把它们放在了WebApplicationContext中,那么它们只会在Controller中,而不是Service中查找@Transactional。

参考资料:
1.Spring Framework Reference Document, Transaction Management