实际上我把自动化单元测试分为了两种
- 针对增删改操作的单元测试
- 针对查询的单元测试
其中“针对增删改操作的单元测试”,可以用dbunit和springtestdbunit来编写单元测试,而“针对查询的单元测试”,我孤陋寡闻没有找到什么现成的工具去解决(哪位朋友知道有这样的工具可以指点一下,谢啦)。下面会一步一步的讲述我自行开发的工具包,解决“针对查询的单元测试”问题。
首先还是从dbunit和springtestdbunit说起
dbunit流程大概是这样的:
- 单元测试前重置数据库
- 单元测试后比对数据库结果与预期结果是否一致
springtestdbunit使用了注解配置,一个springtestdbunit的单元测试大致是这样的
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:com/sztb/dp/test/testContext.xml"})
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class
})
@DbUnitConfiguration(dataSetLoader= XmlDataSetLoader.class)
public class UserTest {
@Test
@DatabaseSetup("classpath:com/look/test/xml/InputUserDelete.xml")
@ExpectedDatabase(assertionMode = DatabaseAssertionMode.NON_STRICT,
value = "classpath:com/look/test/xml/ResultUserDelete.xml")
public void testUserDelete() throws Exception {
//dbunit test code
UserService userService= new UserService();
userService.deleteUser(1L);
}
}
该代码对删除用户操作进行了单元测试,@DatabaseSetup设置了数据库的数据集,@ExpectedDatabase设置了期望的数据集,单元测试代码执行完毕后,程序会检查数据库最终结果,与期望数据集的一致性,完成断言。
但是如果要测试查询操作,数据库在单元测试前后是无变化的,dbunit也就不适用了,我们需要对查询到的结果集的属性进行断言,秉承dbunit的思路,我们可以设想用如下的方式进行基于查询的单元测试的编写,也就是说,我们开发这个工具包,要达到的效果就是下面这样,通过配置我们自己的监听器和自定义的注解,对查询到的结果进行与xml文件的比对
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:com/sztb/dp/test/testContext.xml"})
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class,
ProtoResultTestExecutionListener.class
})
@DbUnitConfiguration(dataSetLoader= XmlDataSetLoader.class)
public class UserTest {
@Test
@DatabaseSetup("classpath:com/look/test/xml/InputGetUsers.xml")
@ExpectedProtoResult("classpath:com/look/test/protoResultXml/ResultGetUsers.xml")
public void testGetUsers() throws Exception {
//dbunit test code
UserService userService= new UserService();
User.GetUserResponse resp = userService.getUsers();
// 将结果集设置到protoResult容器
ResultContainer.getInstance().setResultSet(resp);
}
}
有两点需要注意的地方
- @ExpectedProtoResult注解,设置预期结果集
- @TestExecutionListeners的设置中,另外添加了一个监听器ProtoResultTestExecutionListener.class,这是自行编写的监听器,用来在单元测试执行完毕后,处理@ExpectedProtoResult注解,监听器ProtoResultTestExecutionListener监听我们的单元测试,运行我们的自定义代码,自定义代码中需要解析@ExpectedProtoResult注解中配置的预期结果集,并与ResultContainer中我们的查询结果进行一致性断言。
下面逐一介绍我都编写了哪些类
1.首先,我们可能需要一个这样的工具类,能将得到的结果对象转为符合一定规则的xml格式,并且有良好格式化方便阅读和修改。(本文中的bean对象为protoc生成的javabean)
public class XMLWriter {
public static void writeXML(MessageLite protoObject , String dest) throws Exception
{
String xml = Proto2XML.toXml(protoObject,true);
BufferedWriter bufferedWriter = null;
FileWriter fileWriter = null;
try
{
fileWriter = new FileWriter(dest);
bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write(xml);
bufferedWriter.flush();
} catch (IOException e)
{
e.printStackTrace();
} finally
{
try
{
fileWriter.close();
bufferedWriter.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
}
2.工具类Proto2XML .java
由于这个类涉及到proto javabean到xml的转换,有一些局限性,该处代码省略,思路就是将javaBean转为json格式,再转为带换行和缩进的的xml格式。
public class Proto2XML {
public static org.json.JSONObject proto2JSONObject(MessageLite protoObject) throws org.json.JSONException {
org.json.JSONObject jsonObject = new org.json.JSONObject();
/******* proto javabean cast to json ********/
return jsonObject;
}
public static String toXml(Object protoObject) throws JSONException {
JSONObject jsonObject = proto2JSONObject(protoObject);
return XML.toString(jsonObject,Path.XMLROOT);
}
public static String toXml(Object protoObject,boolean format) throws JSONException {
String xml = toXml(protoObject);
if(format)
{
xml = formatXML(xml);
}
return xml;
}
生成的xml文件形如
<?xml version="1.0" encoding="utf8"?>
<dataset>
<errCode type="string">ERR_OK</errCode>
<errMsg type="string">ERR_OK</errMsg>
<users class="array">
<e class="object">
<id type="string">1</id>
<username type="string">jack</username>
<age type="string">20</age>
</e>
<e class="object">
<id type="string">2</id>
<username type="string">jim</username>
<age type="string">22</age>
</e>
</users>
</dataset>
说一点题外话,上面代码中的MessageLite是protobuf中的一种消息类型,通过对proto文件optimize_for选项的设置,可以选择消息类型为MessageLite或Message,使用MessageLite的效率高一点,但是牺牲了Message的反射功能,比如说,Message可以直接使用getAllfields这样的方法,也可以取到Message的name,而MessageLite就不行,因为MessageLite不提供这样的方法,所以基于Message的protobuf进行xml转换的时候,有现成的框架可以完成Message到xml的转换(因为框架可以直接调用Message原生getAllFields、getName等方法),例如protobuf-java-format。而我们项目中使用的是MessageLite,它转xml的部分,只能由我们自己开发,并没有现成的框架。
我选择了proto javabean先转json再转xml的方式,proto javabean转json用到的json包,我选择了org.json 因为我后续会用到JsonAssert框架来进行json一致性的断言,而JsonAssert使用的就是org.json。json转xml用到的json包,我选择了net.sf.json包,因为org.json包下的xml转换有两个缺点,第一,转换为xml后,JSONArray结构不清晰,如果JSONArray中只有一条JSON,xml转换成JSON的时候,这个JSONAraay也会转换为JSONObject。
第二,对数值的处理有些问题,比如xml中有一个属性值为4.0,转为json的时候4.0会变为4,自动转为了整数,可以说是一种失真,不利于单元测试。
net.sf.json的xml转换功能就不存在这两种问题。
详细了解protobuf及其选项(Options)可参考下面链接
http://www.cnblogs.com/dkblog/archive/2012/03/27/2419010.html
3.自定义监听器ProtoResultTestExecutionListener.java
public class ProtoResultTestExecutionListener extends org.springframework.test.context.support.AbstractTestExecutionListener {
@Override
public void afterTestMethod(TestContext testContext) throws org.json.JSONException, IOException {
Method method = testContext.getTestMethod();
if(method.isAnnotationPresent(ExpectedProtoResult.class))
{
ExpectedProtoResult protoResult = method.getAnnotation(ExpectedProtoResult.class);
String dataSetLocation = protoResult.value();
assertNotNull(dataSetLocation);
if (StringUtils.hasLength(dataSetLocation)) {
String xml = XMLLoader.load(testContext.getClass(),dataSetLocation);
JSONUtil.compareXmlAndProto(xml, ResultContainer.getInstance().getResultSet());
}
}
}
}
4.JSONUtil.java
public class JSONUtil {
public static void compareXmlAndProto(String xml , MessageLite protoObject) throws JSONException {
org.json.JSONObject json1 = string2JSON(xml2JSONString(xml));
org.json.JSONObject json2 = Proto2XML.proto2JSONObject(protoObject);
System.out.println("Expected Result : "+json1.toString());
System.out.println("True Result : " + json2.toString());
JSONAssert.assertEquals(json1, json2, true);
}
public static String xml2JSONString(String xml){
XMLSerializer xmlSerializer = new XMLSerializer();
xmlSerializer.setRootName(Path.XMLROOT);
return xmlSerializer.read(xml).toString();
}
public static String jsonString2XML(String json){
net.sf.json.JSONObject netJson = net.sf.json.JSONObject.fromObject(json);
XMLSerializer xmlSerializer = new XMLSerializer();
xmlSerializer.setRootName(Path.XMLROOT);
return xmlSerializer.write(netJson,"utf8");
}
public static org.json.JSONObject string2JSON(String json) throws JSONException, JSONException {
return new org.json.JSONObject(json);
}
}
5.自定义注解@ExpectedProtoResult
@java.lang.annotation.Documented
@java.lang.annotation.Inherited
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD})
public @interface ExpectedProtoResult {
java.lang.String value();
}
6.protoXML的解析器
这个解析器用到了spring的core包,解析路径时,与spring使用相同的方式
public class XMLLoader{
protected static ResourceLoader getResourceLoader(Class<?> testClass) {
return new ClassRelativeResourceLoader(testClass);
}
protected static String[] getResourceLocations(String location) {
return new String[] { location };
}
public static String load(Class<?> testClass , String location) throws IOException {
StringBuffer sb = new StringBuffer();
ResourceLoader resourceLoader = getResourceLoader(testClass);
String[] resourceLocations = getResourceLocations(location);
for (String resourceLocation : resourceLocations) {
Resource resource = resourceLoader.getResource(resourceLocation);
if (resource.exists()) {
sb.append(getProtoXML(resource.getInputStream()));
}else
{
throw new IOException(resource.getURI().getPath());
}
}
return sb.toString();
}
private static String getProtoXML(InputStream is) throws IOException {
return inputStream2String(is);
}
private static String inputStream2String(InputStream is) throws IOException
{
int len = 0;
StringBuffer str=new StringBuffer("");
try {
InputStreamReader isr = new InputStreamReader(is);
BufferedReader in = new BufferedReader(isr);
String line = null;
while( (line=in.readLine())!=null )
{
str.append(line);
len++;
}
in.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
return str.toString();
}
}
7.装载返回结果对象的ResultContainer
public class ResultContainer {
private static ResultContainer resultContainer = new ResultContainer();
private ResultContainer(){}
public static ResultContainer getInstance()
{
return resultContainer;
}
private MessageLite resultSet = null;
public MessageLite getResultSet() {
return resultSet;
}
public void setResultSet(MessageLite resultSet) {
this.resultSet = resultSet;
}
}
可以看到,使用方式与springtestdbunit基本一致,只是多了ResultContainer.getInstance().setResultSet(resp);这样一个步骤,
那是因为springtestdbunit在@ExpectedDatabase的时候,比对的是数据库的结果,使用@ContextConfiguration中配置的数据库就可以,
而@ExpectedProtoResult比对的不是执行完单元测试后数据库的情况,而是执行完单元测试后,比对我们得到的结果对象,所以我们需要自行设置一下比对的对象resp。