华硕笔记本风扇拆卸:Spring测试

来源:百度文库 编辑:偶看新闻 时间:2024/04/29 14:14:43

         测试的时候会破坏数据库现场,如果两次都执行添加的测试用例,那么会抛出主键重复的异常,另外测试方法中都要手动去获得Service对象,硬编码;使用Spring提供的测试机制:

1.       保护数据库现场

2.       2.通过IOCService注入来获取其对象,而不是硬编码

3.       查看数据库中的实际值

 

Spring的测试包为spring-test.jar文件(某些版本的测试包可能在spring-mock.jar下,具体应用时请检查对应的class文件是否存在),

 

ConditionalTestCase

它本身和Spring没有任何关联,可用于任何单元测试中,他仅仅是添加了一个按条件执行测试方法的功能。在单元测试中,所有带test前缀的方法都会被毫无例外的执行,现在的junit4X版本可以通过加一个@Test注解,而不用遵守这个约定。而继承ConditionalTestCase,可以让你在某些情况下,有选择的关闭掉某些测试方法。

         只需要实现ConditionalTestCaseisDisabledInThisEnvironment方法,ConditionalTestCase在运行每个方法千,会根据isDisabledInThisEnvironment判断是否为要执行的方法,某人情况下,返回false,也就是执行所有方法。

         public void runBare() throws Throwable {

      // getName will return the name of the method being run

      if (isDisabledInThisEnvironment(getName())) {

         recordDisabled();

         logger.info("**** " + getClass().getName() + "." +

            getName() + " disabled in this environment: " +

            "Total disabled tests=" + getDisabledTestCount());

         return;

      }

     

      // Let JUnit handle execution

      super.runBare();

   }

 

/**

    * Should this test run?

    * @param testMethodName name of the test method

    * @return whether the test should execute in the current envionment

    */

   protected boolean isDisabledInThisEnvironment(String testMethodName) {

      return false;

   }

 

AbstractSpringContextTests

扩展自ConditionalTestCase,它维护了一个static类型的缓存器,使用键保存了Spring ApplicationContext实例,这意味着,不同的用例,不同测试方法都可以共享这个实例,也就是说,在运行多个测试用例和测试方法时,Spring容器只需要实例化一次就可以了。

 

private static Map contextKeyToContextMap = new HashMap();

 

 

 

AbstractSingleSpringContextTests

继承自AbstractSpringContextTests,她通过一些方法让你更方便地制定Spring配置文件所在位置:

  • String[] getConfigLocations():该方法允许你在指定Spring配置文件时使用资源类型前缀,这些资源类型前缀包括:classpath:、file:。以类似于“com/baobaotao/beans.xml”形式指定的资源被当成类路径资源处理;
  • String[] getConfigPaths():以“/”开头的地址被当成类路径处理,如“/com/baobaotao/beans.xml”,而未以“/”开头的地址被当成相对于测试类所在包的文件路径,如“beans.xml”表示配置文件在测试类所在类包的目录下;
  • String getConfigPath():和getConfigPaths()类似,在仅需指定一个配置文件中使用。

 

protected final void setUp() throws Exception {

      this.applicationContext = getContext(contextKey());

      prepareTestInstance();

      onSetUp();

}

 

protected final void tearDown() throws Exception {

      onTearDown();

}

 

我可以看到,该类将steUptearDown覆盖为final的,所以我们在子类中就不能自己再去覆盖这些类了。setUp的作用就是确保创建了配置文件所执行的容器。

 

this.applicationContext = getContext(contextKey());

该方法建立了applicationContext,而contextKLey方法方法:

protected Object contextKey() {

      return getConfigLocations();

}

 

protected String[] getConfigLocations() {

      String[] paths = getConfigPaths();

      String[] locations = new String[paths.length];

      for (int i = 0; i < paths.length; i++) {

         String path = paths[i];

         if (path.startsWith("/")) {

            locations[i] = ResourceUtils.CLASSPATH_URL_PREFIX +

                path;

         }

         else {

            locations[i] = ResourceUtils.CLASSPATH_URL_PREFIX +

                   StringUtils.cleanPath(

ClassUtils.classPackageAsResourcePath(

getClass()) + "/" + path);

         }

      }

      return locations;

}

 

protected String[] getConfigPaths() {

      String path = getConfigPath();

      return (path != null ? new String[] {path} : new String[0]);

}

 

protected String getConfigPath() {

      return null;

   }

 

我们从这里可以看出他们的优先级的原因,另外再看一下getContext()方法,他是以final的形式写在父类AbstractSpringContextTests中的:

/**

    * Obtain an ApplicationContext for the given key, potentially cached.

    * @param key the context key

    * @return the corresponding ApplicationContext instance (potentially cached)

    */

   protected final ConfigurableApplicationContext getContext(Object key) throws Exception {

      String keyString = contextKeyString(key);

      ConfigurableApplicationContext ctx =

            (ConfigurableApplicationContext) contextKeyToContextMap.get(keyString);

      if (ctx == null) {

         ctx = loadContext(key);

         ctx.registerShutdownHook();

         contextKeyToContextMap.put(keyString, ctx);

      }

      return ctx;

   }

 

 

可以看出这个方法的意思是先到缓存中查找context,如果没找到context则创建。而作为hashkey,则是以路径的某种形式作为key的。

 

另外,loadContext在这里类中并没有做任何事,而是交给之类去实现的:

protected ConfigurableApplicationContext loadContextLocations(String[] locations) throws Exception {

   ++this.loadCount;

   if (logger.isInfoEnabled()) {

         logger.info("Loading context for locations: " +

      StringUtils.arrayToCommaDelimitedString(locations));

   }

      return createApplicationContext(locations);

}

 

protected ConfigurableApplicationContext createApplicationContext(String[] locations) {

      GenericApplicationContext context = new

         GenericApplicationContext();

      customizeBeanFactory(

context.getDefaultListableBeanFactory());

      new XmlBeanDefinitionReader(context).

loadBeanDefinitions(locations);

      context.refresh();

      return context;

   }

 

 

AbstractDependencyInjectionSpringContextTests

在前面的类都准备好一些基础方法之后,AbstractDependencyInjectionSpringContextTests提供了自动依赖注入的功能,无需手工从容器中获取目标bean进行装配。类似于ByType的方式:

 

package com.baobaotao.test;

import org.springframework.test.AbstractDependencyInjectionSpringContextTests;

import com.baobaotao.service.UserService;

 

public class DependencyInjectionCtxTest extends

AbstractDependencyInjectionSpringContextTests {

private UserService userService;  

 

public void  setUserService(UserService userService) {①该属性设置方法会被自动调动  

               this.userService = userService;

}

@Override

protected String[] getConfigLocations() { ②指定Spring配置文件所在位置

               return new String[]{"baobaotao-service.xml","baobaotao-dao.xml"};

}

public void testHasMatchUser(){ ③测试方法

               boolean match = userService.hasMatchUser("tom","123456");

assertEquals(true,match);

}

}

 

 
 
①按类型匹配于DependencyInjectionCtxTestuserService属性 
 
 
 

 

 

指定了Spring配置文件所在的位置,AbstractDependencyInjectionSpringContextTests将使用这些配置文件初始化好Spring容器,并将它们保存于static的缓存中。然后马上着手根据类型匹配机制(byType),自动将Spring容器中匹配测试类属性的Bean通过Setter注入到测试类中。

 

他覆盖了父类的prepareTestInstance方法,在准备好容器之后,马上注入依赖:

protected void prepareTestInstance() throws Exception {

      injectDependencies();

}

 

 

 

protected void injectDependencies() throws Exception {

      if (isPopulateProtectedVariables()) {

         if (this.managedVariableNames == null) {

            initManagedVariableNames();

         }

         populateProtectedVariables();

      }

      else if (getAutowireMode() != AUTOWIRE_NO) {

         getApplicationContext().getBeanFactory()

.autowireBeanProperties( this,

 getAutowireMode(), isDependencyCheck());

      }

   }

 

自动装配的问题

采用这种byType的自动装配方式,有时候会带来一些问题,当容器中拥有多个匹配类型的Bean的时候,就会抛出UnsatisfiedDependencyException异常。假设我们采用以下传统的事务管理的配置方式对UserService进行配置,按类型匹配的自动装配机制就会引发问题:

①用于被代理的目标Bean,按类型匹配于UserService




②通过事务代理工厂为UserServiceImpl创建的代理Bean,也按匹配于UserService

class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">





由于①处和②处的Bean都按类型匹配于UserService,在对DependencyInjectionCtxTestuserService属性进行自动装配将会引发问题。有两种针对该问题的解决办法:

1.调整配置文件,使按类型匹配于UserServiceBean仅有一个,具体有以下两个方法:

  • 将①处的Bean作为②处的内部Bean进行装配;
  • 使用基于注解驱动的事务管理配置机制,这样就无需在配置文件中定义两个UserService的Bean了。关于注解驱动事务管理配置的详细信息。

2.改变DependencyInjectionCtxTest的自动装配机制:Spring默认使用byType类型的自动装配机制,但它允许你通过setAutowireMode()的方法改变默认自动装配的机制,比如你可以调用setAutowireMode(AUTOWIRE_BY_NAME)方法启用按名称匹配的自动装配机制。

AbstractDependencyInjectionSpringContextTests定义了三个代表自动装配机制类型的常量,分别说明如下:

  • AUTOWIRE_BY_TYPE:按类型匹配的方式进行自动装配,这个默认的机制;
  • AUTOWIRE_BY_NAME:按名字匹配的方式进行自动装配
  • AUTOWIRE_NO:不使用自动装配机制,这意味着你需要手工调用getBean()进行装配。

依赖检查

假设我们在DependencyInjectionCtxTest添加一个User类型的属性并提供Setter方法,而Spring容器中没有匹配该属性的Bean。在默认情况下, AbstractDependencyInjectionSpringContextTests要求所有属性都能在Spring容器中找到对应Bean,否则抛出异常。

仔细思考一下,这种运行机制并非没有道理,因为既然你已经提供了Setter方法,就相当于给出了这样的暗示信息:这个属性测试类自身创建不了,必须由外部提供。而在使用自动装配机制的情况下,测试类属性自动从Spring容器中注入匹配的属性,一般情况下不会手工去调用Setter方法准备属性。

如果你出于一些特殊的理由,希望在采用自动装配的情况下,如果有属性未得到装配也不在乎,那么你可以在测试类构造函数中调用setDependencyCheck(false)方法达到目的:

package com.baobaotao.test;

public class DependencyInjectionCtxTest extends AbstractDependencyInjectionSpringContextTests {
public DependencyInjectionCtxTest(){
setDependencyCheck(false); ①告知不进行属性依赖性检查
}

}

 

 

不写setter方法的注入

大部分IDE都有了为字段生成setter的方式,所以写setter方法其实没什么必要,Spring提供了这种注入方式,本质上是通过反射去获得Field的。只需要在构造函数中调用:

 

将需要自动装配的属性变量声明为protected;

 setPopulateProtectedVariables(true)方法(联系上面的源码)。

 

可能大家对第一点比较奇怪,看一下里面调用的一个方法就知道了:

private void initManagedVariableNames() throws IllegalAccessException {

      LinkedList managedVarNames = new LinkedList();

      Class clazz = getClass();

 

      do {

         Field[] fields = clazz.getDeclaredFields();

         ……….

         for (int i = 0; i < fields.length; i++) {

            Field field = fields[i];

               ….

            if (isProtectedInstanceField(field)) {

                …..

            }

         }

         clazz = clazz.getSuperclass();

      }

      while (!clazz.equals(AbstractDependencyInjectionSpringContextTests.class));

 

      this.managedVariableNames = (String[]) managedVarNames.toArray(new String[managedVarNames.size()]);

   }

 

private boolean isProtectedInstanceField(Field field) {

      int modifiers = field.getModifiers();

      return !Modifier.isStatic(modifiers) &&

         Modifier.isProtected(modifiers);

   }

 

AbstractTransactionalSpringContextTests

继承了上面一个类,他的作用就是能够让事务回滚。UserService以下两个接口方法会对数据库执行更改操作:

void loginSuccess(User user);
void registerUser(User user);

当我们对这两个接口方法进行测试时,它们将会在数据库中产生持久化数据。考虑对registerUser(User user)方法进行测试时,我们可能编写如下所示的测试方法:

public void testRegisterUser(){
User user = new User();
user.setUserId(2);
user.setUserName("john");
user.setPassword("123456");
userService.registerUser(user);
}

当第一次成功运行testRegisterUser()测试方法时,将在数据库中产生一条主键为2的记录,如何第二次重新运行testRegisterUser()测试方法其结果将不言自明:因主键冲突导致测试方法执行失败,最终报告测试用例没有通过。在这种情况下,测试用例未通过并不是因为UserServiceImpl#registerUser(User user)存在逻辑错误,而是因为测试方法的积累效应导致外在设施的现场发生变化而引起的问题。

AbstractTransactionalSpringContextTests专为解决以上问题而生,也就是说前面我们所提个问题在此得到了回答。只要继承该类创建测试用例,在默认情况下,测试方法中所包含的事务性数据操作都会在测试方法返回前被回滚。由于事务回滚操作发生在测试方法返回前的点上,所以你可以象往常一样在测试方法体中对数据操作的正确性进行校验。

package com.baobaotao.service;
import org.springframework.test.AbstractTransactionalSpringContextTests;
import com.baobaotao.domain.User;
public class UserServiceIntegrateTest extends AbstractTransactionalSpringContextTests {
  private UserService userService;
public void setUserService(UserService userService) {
   this.userService = userService;
}
@Override
protected String[] getConfigLocations() {
   return new String[]{"baobaotao-service.xml", "baobaotao-dao.xml"};
}
   public void testRegisterUser(){

①  测试方法中的数据操作将在方法返回前被回滚,不会对数据库
     User user = new User(); 产生永久性数据操作,第二次运行该测试方法时,依旧可以
     user.setUserId(2); 成功运行。
     user.setUserName("john");
     user.setPassword("123456");
     userService.registerUser(user);


  User user1 = userService.findUserByUserName("john"); ②对数据操作进行
   assertEquals(user.getUserId(), user1.getUserId()); 正确性检验
 }

}

如果testRegisterUser()是直接继承于AbstractDependencyInjectionSpringContextTests类的测试方法,则重复运行该测试方法就会发生数据冲突问题。但因为它位于继承于AbstractTransactionalSpringContextTests的测试用例类中,测试方法中对数据库的操作会被正确回滚,所以重复运行不会有任何问题

 

如果你确实希望测试方法中对数据库的操作持久生效而不是被回滚,Spring也可以满足你的要求,你仅需要在测试方法中添加setComplete()方法就可以了。

 public void testRegisterUser(){

User user1 = userService.findUserByUserName("john");
assertEquals(user.getUserId(), user1.getUserId());
setComplete(); ①测试方法中的事务性数据操作将被提交
}

AbstractTransactionalSpringContextTests还拥有几个可用于初始化测试数据库,并在测试完成后清除测试数据的方法,分别介绍如下:

  • onSetUpBeforeTransaction()/onTearDownAfterTransaction():子类可以覆盖这两个方法,以便在事务性测试方法运行的前后执行一些数据库初始化的操作并在事务完成后清除之;
  • onSetUpInTransaction()/onTearDownInTransaction():这对方法和前面介绍的方法完成相同的功能,只不过它们是在测试方法的相同事务中执行的。

AbstractTransactionalSpringContextTests另外还提供了一组用于测试延迟数据加载的方法:endTransaction()/startNewTransaction()。我在测试Hibernate、JPA等允许延迟数据加载的应用时,如何模拟数据在Service层事务中被部分加载,当传递到Web层时重新打开事务完成延迟部分数据加载的测试场景呢?这两个方法即为此用途而生:你可以在测试方法中显式调用endTransaction()方法以模拟从Service层中获取部分数据后返回,尔后,再通过startNewTransaction()开启一个和原事务无关新事务——模拟在Web层中重新打开事务,接下来你就可以访问延迟加载的数据,看是否一切如期所料了。

在代码清单 6的②处,我们通过UserService#findUserByUserName()方法对前面registerUser(user)方法数据操作的正确性进行检验。应该说,我们非常幸运,因为在UserService中刚好存在一个可用于检测registerUser(user)数据操作正确性的方法。让我们考虑另外的一种情况:要是 UserService不存在这样的方法,我们该如何检测registerUser(user)数据操作结果的正确性呢?显然我们不能使用肉眼观察的方法,那难道为了验证数据操作正确性专门编写一个配合性的数据访问类不成?

效验结果正确性AbstractTransactionalSpringContextTests

它添加了一个JdbcTemplate,你可以借由此道快意直达数据库。它自动使用Spring容器中的数据源(DataSource)创建好一个JdbcTemplate实例并开放给子类使用。值得注意的是,如果你采用byName自动装配机制,数据源Bean的名称必须取名为“dataSource”。

 

import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;

public class UserServiceIntegrateWithJdbcTest
extends AbstractTransactionalDataSourceSpringContextTests {

① 注意:继承类发生调整
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
@Override
protected String[] getConfigLocations() {
return new String[]{"baobaotao-service.xml", "baobaotao-dao.xml"};
}
public void testRegisterUser(){
User user = new User();
user.setUserId(2);
user.setUserName("john");
user.setPassword("123456");
userService.registerUser(user);
String sqlStr = " SELECT user_id FROM t_user WHERE user_name ='john' ";
int userId = jdbcTemplate.queryForInt(sqlStr);
可以直接使用JdbcTemplate访问数据库了
assertEquals(user.getUserId(), userId);
setComplete();

}
}

jdbcTemplateAbstractTransactionalDataSourceSpringContextTests类中定义的,子类可以直接使用它访问数据库。这样我们就可以灵活地访问数据库以检验目标测试方法的数据操作正确性。至此,我们终于毕其功于一役于AbstractTransactionalDataSourceSpringContextTests,顺利解决前面我们中指出的最后题。

只要你通过扩展AbstractTransactionalSpringContextTests及其子类创建测试用例,所有测试方法都会工作了事务环境下。也就是说,即使某些测试方法不需要访问数据库,也会产生额外的事务管理开销,是否可以对测试方法启用事务管理的行为进行控制呢?此外,在一些情况下,除对目标方法逻辑运行的正确性进行检验外,我们还希望对目标方法的运行性能进行测试:如当目标方法运行时间超过200毫秒时,则测试用例视为未通过。诸如此类的问题,我们目前学习到的知识还不能很好的应付。Spring 2.0新增了注解驱动的测试工具为我们指明了道路,你仅需要通过简单为测试方法标注注解,我们刚才提出的疑难问题就可以迎刃而解了。

@Test(timeout = 1000)

http://www.uml.org.cn/j2ee/200905074.asp        

详细讲解在Spring中进行集成测试

 

JUnit 4 in 60 Seconds

 

 单元测试-Spring2.5 TestContext测试框架  部分注解内容

Spring 2.5 Makes Unit Testing Easy with Annotations