老大给了这么个任务,搭建一个自动测试平台,他希望的效果是,在改动代码之后,可以一键进行测试,想想看,这个东西是很有意义的,总不能改动一点代码,就要从页面上重新走遍回归测试吧?太傻了。于是调研了一些Spring的测试模块。
Spring的测试模块主要存放在org.springframework.test包下,它包括了集成测试和单元测试,这里我只处理了集成测试,原因有几点:
- 处理起来简单,在构造HTTP请求的时候都是构造字符串,不会存在构造对象的问题(但是最后还是遇到了,在设置session的过程中)
- 可以走完所有流程,从前端的filter、servlet一直到最后响应输出都可以测试到。
- Spring集成了强大的Mock对象,包括了MockHttpRequest\Response\Session等等,并且在request中还实现了诸如设置字符集、设置content内容,构造URL等等等。
主体类
框架的核心是通过解析XML文件,并利用反射机制构造Mock请求。主体类非常简单:
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration(value = "src/com/dacas")
@ContextConfiguration("/applicationContext.xml")
@Transactional
public class TestHandler {
List<TestClass> classes;
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void init(){
ConfigParser configParser = new ConfigParser();
classes = configParser.getTestClass();//读取XML文件中的数据
//初始化MockMVC上下文环境
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void doTest(){
try {
for(TestClass tmpClass:classes){
redirectPath(tmpClass);//重定向到某输出文件中
List<TestUrl> urls = tmpClass.getUrls();
for(TestUrl url:urls){
String urlString = url.getCompositeUrl();//构造URL请求
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get(urlString);//构造requestBuilder
initRequestParams(requestBuilder,url);//初始化请求参数,包括content、parameter、session等
mockMvc.perform(requestBuilder).andDo(MockMvcResultHandlers.print());//真正实施测试,但是这里面并没有添加过多的断言,比如andExpect、或者assert等,这是根据需求定的。
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* initial requestbuilder params
* @param builder
* @param url
*/
private void initRequestParams(MockHttpServletRequestBuilder builder,TestUrl url) {
//设置编码格式
builder.characterEncoding("utf-8");
TestParameter parameter = url.getParamter();
TestContent content = url.getContent();
TestSession sessionData = url.getSession();
//添加session
if(sessionData != null){
MockHttpSession mockHttpSession = new MockHttpSession();
Map<String, Object> maps = sessionData.getSession();
Set<Map.Entry<String, Object>> sets = maps.entrySet();
for(Map.Entry<String, Object> entry:sets){
mockHttpSession.setAttribute(entry.getKey(), entry.getValue());
}
builder.session(mockHttpSession);
}
//添加参数,构造form表单数据
if(parameter != null){
Map<String, String> paramMaps = parameter.getParams();
Set<Map.Entry<String, String>> sets = paramMaps.entrySet();
for(Map.Entry<String, String> entry:sets){
builder.param(entry.getKey(), entry.getValue());//这里为什么用params而不是content,请见上篇博客:
}
return;
}
//获取content数据
if(content != null){
String contentData = content.getContent();
JSONObject jsonObjData = content.getObject();
JSONArray jsonArrayData = content.getArray();
if(contentData.length() != 0)
builder.content(contentData);
else if(jsonObjData != null && jsonObjData.length() != 0)
builder.content(jsonObjData.toString());
else if(jsonArrayData != null && jsonArrayData.length() != 0)
builder.content(jsonArrayData.toString());
}
}
/**
* 查看是否需要将输出重定向,将输出结果输出到文件中
* @param config
* @throws IOException
*/
private void redirectPath(TestClass config) throws IOException{
String path = config.getRedirectPath();
if(path.equals(""))//默认,不需要重定向
return;
PrintStream stream = new PrintStream(path);
System.setOut(stream);
}
}
解析XML文件
解析配置文件是工作量较大的地方,首先读取框架的配置文件,框架的配置文件还比较简单:
<?xml version="1.0" encoding="UTF-8"?>
<config>
<!--是否将IO重定向-->
<IORedirect type="true" path="./out.txt"/>
<!--读取待测试的类的配置文件-->
<!--允许使用通配符*,会根据mapping中的顺序依次进行测试-->
<mapping paths="com/dacas/test/*.test.xml"></mapping>
</config>
处理XML文件的主要类
public class ConfigParser {
private String mainPath = "/DCSTest.config.xml";
/**
* 读取主配置问题classpath路径下的DCSTest.config.xml文件
*/
public List<TestClass> getTestClass(){
InputStream inputStream = getClass().getResourceAsStream(mainPath);
MainConfigParser parser = new MainConfigParser();
List<TestClass> testClasses = new LinkedList<TestClass>();
try {
MainConfig mainConfig = parser.getMainConfigs(inputStream);
redirectPath(mainConfig);
//加载子配置文件
List<InputStream> inputStreams = mainConfig.getInputStreams();
SubTestClassParser subConfig = new SubTestClassParser();
for(InputStream input:inputStreams){
TestClass testClass = subConfig.getTestClass(input);
testClasses.add(testClass);
}
} catch (Exception e) {
e.printStackTrace();
}
return testClasses;
}
/**
* 查看是否需要将输出重定向,将输出结果输出到文件中
* @param config
* @throws IOException
*/
private void redirectPath(MainConfig config) throws IOException{
String path = config.getRedirectPath();
if(path.equals(""))//默认,不需要重定向
return;
PrintStream stream = new PrintStream(path);
System.setOut(stream);
}
}
待测试控制器配置文件
读取完主配置文件后,会根据主配置文件中的mapping项读取相应的子配置文件,虽然没有限制,但是还是建议,将一个控制器类中处理的URL请求放在一个文件中,这样方便修改。
<?xml version="1.0" encoding="UTF-8"?>
<!--是否重定向,当不为""时将会重定向到相应文件-->
<test redirect="">
<!--name为待测试URL-->
<url name="/get_card_info.do">
<!-- URL模板,在URL中进行参数拼接 -->
<template>
<init name="abcd" value="dfjksajdfklas"/>
<init name="eqaz" value="sdfasdfsadf"/>
</template>
<!-- HTTP form表单格式进行参数发送 -->
<parameter>
<init name="popli" value="asdfasdfjk" />
<init name="qwert" value="qwerty"/>
</parameter>
<!-- HTTP body中JSONArray、JSONObject -->
<content>
<!--array中包含的为JSONArray中一个JSONObject对象-->
<!--当没有array标签时,表示一个jsonobject对象-->
<array>
<init name="name" value="a" />
<init name="age" value="18" />
</array>
<array>
<init name="name" value="b" />
<init name="age" value="18" />
</array>
</content>
<!-- 定义HTTP session,name为session中key,value为实体类,类中必须由一个使用testcase作为注解的静态类来返回一个测试对象,为了减少对该类的污染,建议将该方法定义为private -->
<session>
<init name="bound" value="com.dacas.testcase.Bound" />
</session>
</url>
</test>
然后剩下的过程就是解析了,解析完成之后,通过在TestHandler中运行Run as JUnit进行测试。