在web开发中,我们通常进行Service层服务的单元测试。有时候,我们也需要在Controller层进行接口测试,它有如下好处:
规范化接口测试。如果不在Controller层测试,开发人员往往需要使用浏览器或者Postman等工具访问接口,流程繁琐。
提高开发效率。如果不在Controller层测试,开发人员需要启动整个工程判断代码正确性。配置Controller层测试后,无需启动所有接口;使用StandAlone方式测试时,甚至无需启动web工程。
配置监控服务。可以配置监控进程,对所有接口进行周期性检测,发现接口异常时进行通知。
Spring提供两种形式的集成测试:
独立测试方式(StandaloneMockMvcBuilder)。优点:运行快,无需启动web。缺点:对于依赖其他Component的接口,需要手动mock所依赖的Component,手动设置其返回收据。
集成测试方式(DefaultMockMvcBuilder)。优点:加载了ApplicationContext,所依赖的各种Component已经注入,完全模拟运行时场景,无需手动造数据。缺点:相比于独立测试方式,运行较慢。
1. 测试环境准备
测试工程使用的Spring版本如下:
SpringFramework: 4.3.8
SpringBoot:1.5.4 依赖配置如下:
org.springframework.boot
spring-boot-starter-test
1.5.4.RELEASE
test
你没有看错,就只需要一个spring-boot-starter-test。点进去你会发现,它包括了junit、spring-test、mockito等一些列依赖。
写一个Controller,用于测试: @Controller public class SampleController { @Autowired private UserService userService;
@RequestMapping("/")
@ResponseBody
public String home(){
return "Hello World";
}
@RequestMapping("/getUser/{id}")
@ResponseBody
public User getUser(@PathVariable String id){
return userService.queryUserById(id);
}
@RequestMapping("getJson")
public ResponseEntity getJson(@RequestParam(defaultValue = "null message") String message){
Map resultMap = new HashMap<>(4);
resultMap.put("code", 0);
resultMap.put("message", message);
return new ResponseEntity(resultMap, HttpStatus.OK);
}
}
有3个接口,分别返回静态字符串、查询数据库返回对象、返回静态json数据。其中用到UserService,只有一个查询的方法:
@Service
public class UserService{
@Autowired
private JdbcTemplate jdbcTemplate;
public User queryUserById(String id){
String sql = "select id,username from user where id=?";
RowMapper rowMapper = new BeanPropertyRowMapper(User.class);
User user = jdbcTemplate.queryForObject(sql, rowMapper, id);
return user;
}
}
下面要对这个Contoller中的方法写测试用例,如上文所述,分为独立测试和接口测试两种方式。
2. 独立测试方式
独立测试方式使用了mockito,代码如下所示:
@RunWith(MockitoJUnitRunner.class)
public class StandAloneSampleControllerTest{
private MockMvc mockMvc;
@InjectMocks
private SampleController sampleController;
@Mock
private UserService userService;
@Before
public void setUp() throws Exception{
this.mockMvc = MockMvcBuilders.standaloneSetup(sampleController).build();
MockitoAnnotations.initMocks(this);
}
@Test
public void home() throws Exception{
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("Hello World"))
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}
@Test
public void getJson() throws Exception{
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/getJson" )
.param("message", "nothing to show")
.accept(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_UTF8))
// jsonPath refer to https://github.com/json-path/JsonPath.
.andExpect(MockMvcResultMatchers.jsonPath(".message").value("nothing to show"))
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}
/**
* standAlone方式,Service DAO都没有注入bean,不能进行和Service有关的测试.
*
*@throws Exception
*/
@Test
public void getUser() throws Exception{
given(this.userService.queryUserById("1"))
.willReturn(new User(1, "Mock User"));
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/getUser/{id}",1))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath(".id").value(1))
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}
}
其中@before setUp()方法将mockMvc初始化为StandaloneMockMvc,然后使用mockMvc模拟请求。 如果不加 @InjectMocks private SampleController sampleController 和@Mock private UserService userService ,getUser()方法会测试失败,NPE异常。这是因为Standalone方式不会加载Bean,如果没有设置依赖的Component的返回结果,mockMvc默认返回null。所以,需要手动将Controller层中getUser()方法所依赖的UserService注入MockBean,然后手动设置数据。例如上面代码中的:
@Test
public void getUser() throws Exception{
given(this.userService.queryUserById("1")).willReturn(new User(1, "Mock User"));
// ...
这句话的意思是,在userService的queryUserById("1")方法返回new User(1, "Mock User")时进行测试。如此依赖,也测试成功了。此时,通过输出的数据可以看到,getUser()获取的User对象是"Mock User"。
注意代码中,getJson()方法内
.andExpect(MockMvcResultMatchers.jsonPath(".message").value("nothing to show"))
可以使用MockMvcResultMatchers中自带的jsonPath判断返回json中“message”于是否为"nothing to show"字符串。
3. 集成测试方式
基于spring-boot-starter-test的集成测试配置也非常简单,只需要使用@SpringBootTest注解配置程序启动的入口即可。这里我们实现了一个BaseContextControllerTest父类:
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = AppStarter.class)
public class BaseContextControllerTest {
@Autowired
protected WebApplicationContext wac;
}
其中AppStarter是web的启动类。使用@Autowired配置WebApplicationContext。SampleController对应的测试类如下:
public class IntegrateSampleControllerTest extends BaseContextControllerTest{
private MockMvc mockMvc;
@Before
public void setup() throws Exception{
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void home() throws Exception{
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("Hello World"))
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}
@Test
public void getJson() throws Exception{
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/getJson" )
.param("message", "nothing to show")
.accept(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_UTF8))
// jsonPath refer to https://github.com/json-path/JsonPath.
.andExpect(MockMvcResultMatchers.jsonPath(".message").value("nothing to show"))
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}
/**
* 集成测试方式,加载了application context,所依赖的service都会注入.
*
*@throws Exception
*/
@Test
public void getUser() throws Exception{
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/getUser/{id}",1))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath(".id").value(1))
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}
}
注意,getUser()不再需要手动mock数据,而是使用了UserService真实返回的数据。另外,如果你的spring-boot版本比较低(比如1.1.9),可能不支持@SpringBootTest注解。这时可以使用@ContextConfiguration(classes = {BaseTestConfig.class})加载配置信息。BaseTestConfig的实现示例如下:
@Configuration
@ComponentScan({ "package name" })
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class BaseTestConfig{
@Bean
public static PropertySourcesPlaceholderConfigurer propertyConfigurer(){
return new PropertySourcesPlaceholderConfigurer();
}
}
4. 基于JsonSchema进行验证
通常,我们希望验证接口返回的json数据格式是否正确,这时程序正确的前提。json-schema-validator提供了json格式的验证方法(json schema可以参考RFC draft)。只需引入以下依赖:
com.github.java-json-tools
json-schema-validator
2.2.8
新建baseSchena.json资源文件,存放jsonSchema模板:
{
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions": {},
"properties": {
"code": {
"id": "/properties/code",
"type": "integer"
},
"message": {
"id": "/properties/message",
"type": "string"
},
"result": {
"id": "/properties/result",
"type": "object"
}
}
},
"type": "object"
}
可以使用jsonSchema在线生成工具根据实际json数据,生成jsonSchema。然后在BaseContextControllerTest中写个方法用于测试:
/**
* 测试是否返回的Json是否符合Basic格式
* Basic格式:{"message":String,"result":Object,"code":Integer}
*
*@param testStr
*@return
*@throws Exception
*/
protected Boolean testBasicJsonSchema (String testStr) throws Exception{
final JsonNode testNode = JsonLoader.fromString(testStr);
final JsonNode basicSchemaPlain = JsonLoader.fromResource(BASIC_SCHEMA_PATH);
final JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.byDefault();
final JsonSchema basicSchema = jsonSchemaFactory.getJsonSchema(basicSchemaPlain);
ProcessingReport report = basicSchema.validate(testNode);
return report.isSuccess();
}
在getJson()方法中调用:
String responseStr = mvcResult.getResponse().getContentAsString();
System.out.println(responseStr);
Boolean success = super.testBasicJsonSchema(responseStr);
assertTrue(success);
显然是通不过的,因为没有“result”域。json-schema-validator的用法可以参考github。
5. 其他参考
本文来自网易实践者社区,经作者葛志诚授权发布。