spring-test单元测试异常No qualifying bean of type javax.servlet.http.HttpServletRequest

Java,高新技术,Spring

2017-12-22

358

1

目录


背景

使用spring test进行单元测试,测试service层,测试代码如下:
public class TestHotelIndexService extends TestApiBase {
    @Autowired
    private HotelIndexService hotelIndexService;

    @Test
    public void testSearchClockRoom() throws CurrentTimeOverNightUserLimitException {
        SearchParams searchParams = 
            ParamBuilders.searchParamBuilder()
                .cityCode("440100")
                .provinceCode("440000")
                .category(HotelIndexConst.HOTEL_SEARCHTYPE_FULLTIME)
                .beginDate(1502467200000L)
                .endDate(1502553600000L)
                .get();
        long start = System.currentTimeMillis();
        PageHolder pageHolder = hotelIndexService.search(searchParams);
        long end = System.currentTimeMillis();
        log.info("Search spend time : " + (end - start) + "ms");
        System.out.println(pageHolder);
    }
}
TestApiBase类已经配置了Spring test的注解和spring配置文件:
@ContextConfiguration(locations = {"classpath*:spring/config-*.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
@Transactional
public class TestApiBase {
    private static final Logger LOG = LoggerFactory.getLogger(TestApiBase.class);
}
运行testSearchClockRoom测试方法时spring抛出异常:
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: private javax.servlet.http.HttpServletRequest cn.bookingsmart.syslog.writer.SysLogWriter.request; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [javax.servlet.http.HttpServletRequest] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:558)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:331)
... 40 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [javax.servlet.http.HttpServletRequest] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoSuchBeanDefinitionException(DefaultListableBeanFactory.java:1308)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1054)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:949)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:530)
... 42 more
异常的红色部分为抛出异常的地方,代码如下:
@Component
@Aspect
public class SysLogWriter {
    public static final Logger logger = LoggerFactory.getLogger(SysLogWriter.class);
    @Autowired
    private HttpServletRequest request;
    ……
}
这个类需要使用HttpServletRequest 对象,自动注入了该对象,所以Spring在执行单元测试的时候需要找到该对象的实现。

原因分析

HttpServletRequest实现

其实,HttpServletRequest 对象是java EE官方发布的servlet规范的一个接口,每一种java容器都有自己对servlet规范的实现。例如,在tomcat中,实现HttpServletRequest 的方式如下:
java web在容器内运行时,调用的Http请求和影响均是容器提供的实现。
在sevlet规范中,还提供了包装模式的wrapper对象:
package javax.servlet.http;
……
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {
    public HttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    ……
}
大多数情况下,我们无需实现HttpServletRequest 接口,而是直接继承HttpServletRequestWrapper 包装对象,来实现Http各项功能。
然而,spring-test在独立运行单元测试的时候,其实并没有集成容器的jar包,也不包含servlet环境,而是纯spring应用方式启动。spring-test也有自己的servlet规范简单实现,例如对于HttpServletRequest 接口,我们如果使用spring的文件上传功能,则会用到如下spring的http实现:
public interface MultipartHttpServletRequest extends HttpServletRequest, MultipartRequest {
/**
 * Return this request's method as a convenient HttpMethod instance.
 */
HttpMethod getRequestMethod();

/**
 * Return this request's headers as a convenient HttpHeaders instance.
 */
HttpHeaders getRequestHeaders();

/**
 * Return the headers associated with the specified part of the multipart request.
 *

If the underlying implementation supports access to headers, then all headers are returned. * Otherwise, the returned headers will include a 'Content-Type' header at the very least. */ HttpHeaders getMultipartHeaders(String paramOrFileName); }

public abstract class AbstractMultipartHttpServletRequest extends HttpServletRequestWrapper
      implements MultipartHttpServletRequest {
    ……
}
public class DefaultMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
   private static final String CONTENT_TYPE = "Content-Type";
   private Map<String, String[]> multipartParameters;
   private Map<String, String> multipartParameterContentTypes;
    ……
}
DefaultMultipartHttpServletRequest 类就是spring单独作为标准文件上传请求的默认Http实现,还有一种实现StandardMultipartHttpServletRequest,不再详细研究:
非文件上传的Http请求,spring-test有一种mock实现:
public class MockHttpServletRequest implements HttpServletRequest {
   private static final String HTTP = "http";
   private static final String HTTPS = "https";
    ……
}
我们在测试controller层的时候,使用的就是该请求和其对应的响应对象:
public class MockHttpServletResponse implements HttpServletResponse {
   private static final String CHARSET_PREFIX = "charset=";
   private static final String CONTENT_TYPE_HEADER = "Content-Type";
   private static final String CONTENT_LENGTH_HEADER = "Content-Length";
   private static final String LOCATION_HEADER = "Location";
    ……
}

WebAppConfiguration注解

如下是测试Controller层的代码:
基础对象:
@ContextConfiguration(locations = {"classpath*:/spring/*.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@Transactional
public class TestControllerBase {
    private static final Logger LOG = LoggerFactory.getLogger(TestControllerBase.class);

    protected MockMvc mockMvc;
    @Autowired
    protected WebApplicationContext wac;

    @Before
    public void setup() throws Exception {
        this.mockMvc = webAppContextSetup(this.wac).alwaysExpect(status().isOk()).build();
    }
}
测试代码:
public class TestHotelIndexApi extends TestControllerBase {
    public static final String PREFIX = "/hotel/data";
    @Test
    public void testGet() throws Exception {
        Long id = 20000L;
        mockMvc.perform(MockMvcRequestBuilders.get(PREFIX + "/" + id)
                .accept(MediaType.APPLICATION_FORM_URLENCODED)
        ).andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string(StringContains.containsString(SUCCESS_STRING)))
                .andDo(MockMvcResultHandlers.print());
    }
}
TestControllerBase与前边的测试service层的基础类TestApiBase对比,增加了一个注解WebAppConfiguration,官方说明如下:
WebAppConfiguration是一个类级注释,用于声明加载到集成测试的ApplicationContext应该是WebApplicationContext。
在测试类中仅仅使用@ WebAppConfiguration,就可以确定为测试使用WebApplicationContext加载到web应用程序根的路径的默认值。要覆盖默认值,请通过value属性显式指定资源路径。
注意,@ WebAppConfiguration必须与@ ContextConfiguration结合使用,要么用在一个测试类中,要么用在一个测试类集成体系中。
那么,WebApplicationContext类是做什么的?这要设计到spring的上下文了。官方说明:
WebApplicationContext是为web应用程序提供配置的接口。该接口将getServletContext()方法添加到通用ApplicationContext接口,并定义一个应用程序属性名,该属性名必须绑定到bootstrap进程中。
与通用应用程序上下文一样,web应用程序上下文是分层的。每个应用程序都有一个单独的根上下文,而应用程序中的每个servlet(包括MVC框架中的dispatcher servlet)都有自己的子上下文。
简单来说,WebApplicationContext就是Web应用程序的上下文对象。通过该对象,我们可以获取java规范的ServletContext对象,从而运行java web程序。

问题解决

因此,SysLogWriter需要注入HttpServletRequest对象,该对象属于servlet规范,也就是说我们告诉Spring我们正在测试的是Web应用,因此,需要在测试类上增加@WebAppConfiguration注解。
添加该注解后,可能遇到如下异常:
java.lang.NoClassDefFoundError: javax/servlet/SessionCookieConfig
问题原因:servlet-api.jar包版本太低,SessionCookieConfig实在servlet3.0版本新增的,所以需要修改项目的jar包,将servlet-api.jar升级为3.0,我项目采用的maven,配置如下:
 

  javax.servlet
  javax.servlet-api
  3.0.1
  provided


  org.glassfish.web
  jstl-impl
  1.2
  
    
      javax.servlet
      servlet-api
    
  

总结

1、spring-test默认启动时没有加载web环境,适用于测试时不需要servlet支持的情况,例如直接测试service;
2、需要使用spring-test测试web环境,需要使用注解@WebAppConfiguration标记测试类,可以用于继承,我的项目是写了两个测试基础类TestApiBase和TestControllerBase,分别用于测试service和controller,测试类只需继承其中一个即可;
3、spring4.0以后的test应该升级servlet包为3.0,否则可能找不到SessionCookieConfig类
 

前一篇:Windows server 2012 R2评估版本激活
后一篇:RabbitMQ基础(一)——基本概念和HelloWorld

belonk

轻轻地我走了,正如我轻轻地来,我挥一挥衣袖,不带走一片云彩