一、前言
在二十三天中我们介绍了使用maven来下载工程的依赖库文件,用ant来进行war包的建立。今天我们在这个基础上将使用junit+dbunit来进行带有单元测试报告的框架的架构。
目标:
- 每次打包之前自动进行单元测试并生成单元测试报告
- 生成要布署的打包文件即war包
- 单元测试的代码不能够被打在正式的要布署的war包内,单元测试仅用于unit test用
- 使用模拟数据对dao层进行测试,使得dao方法的测试结果可被预料
二、Junit+Ant生成的单元测试报告
上面是一份junit生成的测试报告,它可以与ant任务一起运行然后自动生成这么一份html的测试报告,要生成这样的一份junit test report我们需要调用ant任务中的<junitreport>这个task,示例代码如下:
[plain] view plain copy print ?
- <target name="junitreport">
- <junit printsummary="on" haltonfailure="false" failureproperty="tests.failed" showoutput="true">
- <classpath>
- <pathelement path="${dist.dir}/${webAppQAName}/WEB-INF/classes" />
- <fileset dir="${lib.dir}">
- <include name="*.jar" />
- </fileset>
- <fileset dir="${ext-lib.dir}">
- <include name="*.jar" />
- </fileset>
- </classpath>
- <formatter type="xml" />
- <batchtest todir="${report.dir}">
- <fileset dir="${dist.dir}/${webAppQAName}/WEB-INF/classes">
- <include name="org/sky/ssh/ut/Test*.*" />
- </fileset>
- </batchtest>
- </junit>
- <junitreport todir="${report.dir}">
- <fileset dir="${report.dir}">
- <include name="TEST-*.xml" />
- </fileset>
- <report format="frames" todir="report" />
- </junitreport>
- <fail if="tests.failed">
- ---------------------------------------------------------
- One or more tests failed, check the report for detail...
- ---------------------------------------------------------
- </fail>
- </target>
<target name="junitreport">
<junit printsummary="on" haltonfailure="false" failureproperty="tests.failed" showoutput="true">
<classpath>
<pathelement path="${dist.dir}/${webAppQAName}/WEB-INF/classes" />
<fileset dir="${lib.dir}">
<include name="*.jar" />
</fileset>
<fileset dir="${ext-lib.dir}">
<include name="*.jar" />
</fileset>
</classpath>
<formatter type="xml" />
<batchtest todir="${report.dir}">
<fileset dir="${dist.dir}/${webAppQAName}/WEB-INF/classes">
<include name="org/sky/ssh/ut/Test*.*" />
</fileset>
</batchtest>
</junit>
<junitreport todir="${report.dir}">
<fileset dir="${report.dir}">
<include name="TEST-*.xml" />
</fileset>
<report format="frames" todir="report" />
</junitreport>
<fail if="tests.failed">
---------------------------------------------------------
One or more tests failed, check the report for detail...
---------------------------------------------------------
</fail>
</target>
在一般的产品级开发时或者是带有daily building/nightly building的项目组中我们经常需要检查最新check in的代码是否影响到了原有的工程的编译,因为每天都有程序员往源码服务器里check in代码,而有时我们经常会碰到刚刚被check in的代码在该程序员本地跑的好好的,但是check in源码服务器上后别人从源码服务器“拉”下来的最新代码跑不起来,甚至编译出错,这就是regression bug,因此我们每天的打包要干的事情应该是:
- 程序员check in代码时必须把相关的unit test也check in源码服务器
- 次日的零晨由持续集成构件如:cruisecontrol自动根据设好的schedule把所有的源码服务器的代码进行编译
- 运行单元测试
- 生成报告
- 打包布署到QA服务器上去
如果考究点的还会生成一份“单元测试覆盖率”报告。
那么有了这样的单元测试报告,项目组组长每天早上一上班检查一下单元测试报告就知道昨天代码check in的情况,有多少是成功多少是失败,它们分别是哪些类,哪些方法,以找到相关的负责人。
同时,有了单元测试报告,如果测试报告上显示的是有fail的地方,该版本就应被视之为fail,不能被送给QA进行进一步的测试,直到所有的单元测试成功才能被送交QA。
三、如何在Spring下书写一个单元测试方法
3.1使用spring的注入特性书写一个单元测试
Spring是一个好东西,一切依赖注入,连单元测试都变成了依赖注入了,这省去我们很多麻烦。
我们可以将web工程中的applicationContext、Datasource甚至iBatis或者是Hibernate的配署都可以注入给junit,这样使得我们可以用IoC的方法来书写我们的单元测试类。
此处,我们使用的junit为4.7, 而相关的spring-test库文件为3.1,我都已经在pom.xml文件中注明了.
我们先在eclipse里建立一个专门用来放单元测试类的src folder:test/main/java。
注意一下单元测试类的coding convention:
- 所有的测试类必须以Test开头
- 所有的测试方法名必须为public类型并且以test开头
- 所有的测试类全部放在test/main/java目录下,不可和src/main/java混放
类 org.sky.ssh.ut.BaseSpringContextCommon
[java] view plain copy print ?
- package org.sky.ssh.ut;
-
- import org.junit.runner.RunWith;
- import org.springframework.test.context.ContextConfiguration;
- import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
- import org.springframework.test.context.transaction.TransactionConfiguration;
- import org.springframework.transaction.annotation.Transactional;
-
- @RunWith(SpringJUnit4ClassRunner.class)
- @ContextConfiguration({ "/spring/appconfig/applicationContext.xml", "/org/sky/ssh/ut/ds/datasource.xml",
- "/spring/hibernate/hibernate.xml" })
- public class BaseSpringContextCommon {
- }
package org.sky.ssh.ut;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "/spring/appconfig/applicationContext.xml", "/org/sky/ssh/ut/ds/datasource.xml",
"/spring/hibernate/hibernate.xml" })
public class BaseSpringContextCommon {
}
该类为一个基类,我们所有的单元测试类全部需要继承自该类,大家可以把这个类认为一个spring的context加载器,
注意这边的datasource.xml。
因为我们在做测试方法时势必会涉及到对一些数据进行操作,因此我们在数据库里除了平时开发和布署用的数据库外,还有一个专门用于运行“单元测试”的“单元测试数据库”或者“单元测试数据库
实例”,因此我们在单元测试时会把我们当前的数据库连接“硬”指向到“单元测试用数据库”上去.
这个datasource.xml文件位于/org/sky/ssh/ut/ds目录下,见下图(当然它也必须被放在test/main/java目录里哦:
该文件内容如下:
org.sky.ssh.ut.ds.datasource.xml
[html] view plain copy print ?
- <?xml version="1.0" encoding="UTF-8"?>
- <beans xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
- xmlns:context="http://www.springframework.org/schema/context" xmlns="http://www.springframework.org/schema/beans"
- xsi:schemaLocation="
- http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
- http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
- http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
- http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
-
-
-
-
- <bean class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource" />
-
-
- <!--
- <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
- <property name="jndiName">
- <value>${jdbc.jndiname}</value>
- </property>
- </bean>
-
- -->
-
-
- <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
-
- <property name="driverClass" value="${jdbc.driverClassName}" />
-
- <property name="jdbcUrl" value="${jdbc.databaseURL}" />
-
- <property name="user" value="alpha_test" />
-
- <property name="password" value="password_1" />
-
- <property name="initialPoolSize" value="10" />
-
- <property name="minPoolSize" value="10" />
-
- <property name="maxPoolSize" value="15" />
-
- <property name="acquireIncrement" value="1" />
-
- <property name="maxIdleTime" value="5" />
- </bean>
-
- <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
-
- <property name="dataSource" ref="dataSource" />
- </bean>
-
- <tx:advice id="txAdvice" transaction-manager="transactionManager">
-
- <tx:attributes>
-
- <tx:method name="submit*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
-
- <tx:method name="add*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
-
- <tx:method name="del*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
-
- <tx:method name="upd*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
-
- <tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
-
- <tx:method name="query*" read-only="true" />
-
- <tx:method name="find*" read-only="true" />
-
- <tx:method name="get*" read-only="true" />
-
- <tx:method name="view*" read-only="true" />
-
- <tx:method name="search*" read-only="true" />
-
- <tx:method name="check*" read-only="true" />
-
- <tx:method name="is*" read-only="true" />
-
- <tx:method name="*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
- </tx:attributes>
- </tx:advice>
-
- <aop:config>
-
- <aop:pointcut id="serviceMethod" expression="execution(* org.sky.ssh.service.impl.*.*(..))" />
-
- <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethod" />
- </aop:config>
-
- </beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context" xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<bean class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource" />
<!-- configure data base connection pool by using JNDI -->
<!--
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>${jdbc.jndiname}</value>
</property>
</bean>
-->
<!-- configure data base connection pool by using C3P0 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${jdbc.driverClassName}" />
<property name="jdbcUrl" value="${jdbc.databaseURL}" />
<property name="user" value="alpha_test" />
<property name="password" value="password_1" />
<property name="initialPoolSize" value="10" />
<property name="minPoolSize" value="10" />
<property name="maxPoolSize" value="15" />
<property name="acquireIncrement" value="1" />
<property name="maxIdleTime" value="5" />
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="submit*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
<tx:method name="add*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
<tx:method name="del*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
<tx:method name="upd*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
<tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
<tx:method name="query*" read-only="true" />
<tx:method name="find*" read-only="true" />
<tx:method name="get*" read-only="true" />
<tx:method name="view*" read-only="true" />
<tx:method name="search*" read-only="true" />
<tx:method name="check*" read-only="true" />
<tx:method name="is*" read-only="true" />
<tx:method name="*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceMethod" expression="execution(* org.sky.ssh.service.impl.*.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethod" />
</aop:config>
</beans>
注意两行:
<property name="user" value="alpha_test" />
<property name="password" value="password_1" />
可以得知我们测试时用的是同一个数据库上的另一个实例,该实例是专门为我们的单元测试用的.
我们先来书写一个单元测试类吧
org.sky.ssh.ut.TestLoginDAO
[java] view plain copy print ?
- package org.sky.ssh.ut;
-
- import static org.junit.Assert.assertEquals;
-
- import javax.annotation.Resource;
-
- import org.junit.Test;
- import org.sky.ssh.dao.LoginDAO;
- import org.springframework.test.annotation.Rollback;
-
- public class TestLoginDAO extends BaseSpringContextCommon {
- @Resource
- private LoginDAO loginDAO;
-
- @Test
- @Rollback(false)
- public void testLoginDAO() throws Exception {
- String loginId = "alpha";
- String loginPwd = "aaaaaa";
- long answer = loginDAO.validLogin(loginId, loginPwd);
- assertEquals(1, answer);
- }
- }
package org.sky.ssh.ut;
import static org.junit.Assert.assertEquals;
import javax.annotation.Resource;
import org.junit.Test;
import org.sky.ssh.dao.LoginDAO;
import org.springframework.test.annotation.Rollback;
public class TestLoginDAO extends BaseSpringContextCommon {
@Resource
private LoginDAO loginDAO;
@Test
@Rollback(false)
public void testLoginDAO() throws Exception {
String loginId = "alpha";
String loginPwd = "aaaaaa";
long answer = loginDAO.validLogin(loginId, loginPwd);
assertEquals(1, answer);
}
}
很简单吧,把原来的LongDAO注入进我们的单元测试类中,然后在test方法前加入一个@Test代码该方法为“单元测试”方法即可被junit可识别,然后我们调用一下LoginDAO中的.validLogin方法,测试一下返回值。
运行方法为:
在eclipse打开该类的情况下右键->run as Junit Test
然后选junit4来运行,运行后直接出错抛出:
[java] view plain copy print ?
- Class not found org.sky.ssh.ut.TestLoginDAO
- java.lang.ClassNotFoundException: org.sky.ssh.ut.TestLoginDAO
- at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
- at java.security.AccessController.doPrivileged(Native Method)
- at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
- at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
- at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
- at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
- at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.loadClass(RemoteTestRunner.java:693)
- at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.loadClasses(RemoteTestRunner.java:429)
- at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:452)
- at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
- at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
- at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
Class not found org.sky.ssh.ut.TestLoginDAO
java.lang.ClassNotFoundException: org.sky.ssh.ut.TestLoginDAO
at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.loadClass(RemoteTestRunner.java:693)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.loadClasses(RemoteTestRunner.java:429)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:452)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
这样一个错误,为什么?
其原因在于我们的工程是在eclipse里使用的m2 eclipse这个插件生成的,因此在做单元测试时由于我们的unit test的类是放在test/main/java这个目录下,而这个目录是我们手工建的,因此eclipse不知道这个目录的对应的编译输出的class的目录了.
没关系,按照下面的方法:
右键->选择run as->run configuration,打开如下的设置
选择classpath这个选项栏
- 单击user Entries
- 单击Advanced按钮
- 在弹出框中选择Add Folders
- 点ok按钮
在下一个弹出框中选择我们的junit test的源码在被编译后输出的目录即myssh2工程的WebContent/WEB-INF/classes目录,对吧。
点OK按钮
点Apply按钮
点Run按钮,查看运行效果
运行成功,说明该unit test书写的是对的。
3.2 结合dbunit来做单元测试
我们有了junit为什么还要引入一个dbunit呢?这不是多此一举吗?
试想一下下列场景:
我们开发时连的是开发用的数据库,一张表里有一堆的数据,有些数据不是自己的插的是其它的开发人员插的,那么我想要测试一个dao或者是service方法,获得一个List,然后判断这个List里的值是否为我想要的时候,有可能会碰到下属这样的情况:
运行我的service或者dao方法得到一个list,该list含有6个值,但正好在运行时另一个开发人员因为测试需要往数据库里又插了一些值,导致我的测试方法失败,对不对,这种情况是有可能的。
怎么办呢?比较好的做法是我们需要准备一份自己的业务数据即prepare data,因为是我们自己准备的数据数据,因此它在经过这个方法运行后得到的值,这个得到的值是要经过一系列的业务逻辑的是吧?因此这个得到的值即:expected data是可以被精确预料的。
因此,我们拿着这个expected data与运行了我们的业务方法后得到的结果进行比对,如果比对结果一致,则一定是测试成功,否则失败,对吧?
这就是我们常说的,测试用数据需要是一份干净的数据。
那么为了保持我们的数据干净,我们在测试前清空我们的业务表,插入数据,运行测试地,比对结果,删除数据(也可以不删除,因为每次运行时都会清空相关的业务表),这也就是为什么我们事先要专门搞一个数据库或者是数据库实例,在运行单元测试时我们的数据库连接需要指向到这个单元测试专用的数据库的原因了,见下面的测试流程表:
有了DbUnit,它就可以帮助我们封装:
- 准备测试用数据
- 清空相关业务表
- 插入测试数据
- 比对结果
- 清除先前插入的业务数据
这一系列底层的操作。
现在我们可以开始搭建我们的单元测试框架了,下面是这个单元测试框架的”逻辑表达图“(一个架构设计文档不仅需要有logic view还要有physical view。。。当然还有更多,以后会一点点分享出来)
这边的Session Factory是结合的原有框架的Hibernate的Session Factory,我们也可以把它改成iBatis,Jdbc Template等等等。。。它可以稍作变动就可适用于一切SSX这样的架构。
该框架的优点如下:
3.3 构建spring+junit+dbunit的框架
除去上述的一些类和配置我们还需要3个基类,它们分别位于test/main/java目录下(因为它们都属于unit test对吧)
org.sky.ssh.ut.util.CleanTableXmlAdapter
[java] view plain copy print ?
- package org.sky.ssh.ut.util;
-
- import org.dom4j.Element;
- import org.dom4j.VisitorSupport;
- import java.util.*;
-
- public class CleanTableXmlAdapter extends VisitorSupport {
-
- private ArrayList tableList = new ArrayList();
-
- public CleanTableXmlAdapter() {
- }
-
- public void visit(Element node) {
- try {
-
- if ((node.getName().toLowerCase()).equals("table")) {
- TableBean tBean = new TableBean();
- tBean.setTableName(node.getText());
- tableList.add(tBean);
- }
-
- } catch (Exception e) {
- }
- }
-
- public ArrayList getTablesList() {
- if (tableList == null || tableList.size() < 1) {
- return null;
- } else {
- return tableList;
- }
- }
- }
package org.sky.ssh.ut.util;
import org.dom4j.Element;
import org.dom4j.VisitorSupport;
import java.util.*;
public class CleanTableXmlAdapter extends VisitorSupport {
private ArrayList tableList = new ArrayList();
public CleanTableXmlAdapter() {
}
public void visit(Element node) {
try {
if ((node.getName().toLowerCase()).equals("table")) {
TableBean tBean = new TableBean();
tBean.setTableName(node.getText());
tableList.add(tBean);
}
} catch (Exception e) {
}
}
public ArrayList getTablesList() {
if (tableList == null || tableList.size() < 1) {
return null;
} else {
return tableList;
}
}
}
org.sky.ssh.ut.util.TableBean
[java] view plain copy print ?
- package org.sky.ssh.ut.util;
- import java.io.*;
- public class TableBean implements Serializable{
-
- private String tableName = "";
-
- public String getTableName() {
- return tableName;
- }
-
- public void setTableName(String tableName) {
- this.tableName = tableName;
- }
- }
package org.sky.ssh.ut.util;
import java.io.*;
public class TableBean implements Serializable{
private String tableName = "";
public String getTableName() {
return tableName;
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
}
org.sky.ssh.ut.util.XmlUtil
[java] view plain copy print ?
- package org.sky.ssh.ut.util;
-
- import java.util.*;
- import java.io.*;
- import org.dom4j.Document;
- import org.dom4j.Element;
- import org.dom4j.VisitorSupport;
- import org.dom4j.io.SAXReader;
- import org.springframework.core.io.ClassPathResource;
-
- public class XmlUtil {
-
- public ArrayList getCleanTables(String xmlFile) {
- ArrayList tablesList = new ArrayList();
- try {
- SAXReader reader = new SAXReader();
- File file = new File(xmlFile);
- Document doc = reader.read(file);
- CleanTableXmlAdapter xmlAdapter = new CleanTableXmlAdapter();
- doc.accept(xmlAdapter);
- tablesList = xmlAdapter.getTablesList();
- return tablesList;
- } catch (Exception e) {
- e.printStackTrace();
- return null;
- }
- }
-
- }
package org.sky.ssh.ut.util;
import java.util.*;
import java.io.*;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.VisitorSupport;
import org.dom4j.io.SAXReader;
import org.springframework.core.io.ClassPathResource;
public class XmlUtil {
public ArrayList getCleanTables(String xmlFile) {
ArrayList tablesList = new ArrayList();
try {
SAXReader reader = new SAXReader();
File file = new File(xmlFile);
Document doc = reader.read(file);
CleanTableXmlAdapter xmlAdapter = new CleanTableXmlAdapter();
doc.accept(xmlAdapter);
tablesList = xmlAdapter.getTablesList();
return tablesList;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
3.4使用框架
我们准备两份测试用数据
test_del_table.xml文件
[html] view plain copy print ?
- <?xml version="1.0" encoding="UTF-8"?>
- <Tables>
- <table>t_student</table>
- </Tables>
<?xml version="1.0" encoding="UTF-8"?>
<Tables>
<table>t_student</table>
</Tables>
test_insert_table.xml文件
[html] view plain copy print ?
- <?xml version="1.0" encoding="UTF-8"?>
- <dataset>
- <t_student student_no="101" student_name="alice"/>
- <t_student student_no="102" student_name="jil"/>
- <t_student student_no="103" student_name="leon"/>
- <t_student student_no="104" student_name="chris"/>
- <t_student student_no="105" student_name="Ada Wong"/>
- </dataset>
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<t_student student_no="101" student_name="alice"/>
<t_student student_no="102" student_name="jil"/>
<t_student student_no="103" student_name="leon"/>
<t_student student_no="104" student_name="chris"/>
<t_student student_no="105" student_name="Ada Wong"/>
</dataset>
测试类org.sky.ssh.ut.TestStudentService
package org.sky.ssh.ut;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.DefaultTable;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.ext.mysql.MySqlDataTypeFactory;
import org.dbunit.operation.DatabaseOperation;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.sky.ssh.service.StudentService;
import org.sky.ssh.ut.util.TableBean;
import org.sky.ssh.ut.util.XmlUtil;
import org.sky.ssh.vo.StudentVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.test.annotation.Rollback;
public class TestStudentService extends BaseSpringContextCommon {
private final static String INSERT_TBL = "org/sky/ssh/ut/xmldata/student/test_insert_table.xml";
private final static String DEL_TBL = "org/sky/ssh/ut/xmldata/student/test_del_table.xml";
@Autowired
private DataSource dataSource;
@Resource
private StudentService stdService;
@SuppressWarnings("deprecation")
@Before
public void setUp() throws Exception {
IDatabaseConnection connection = null;
try {
connection = new DatabaseConnection(DataSourceUtils.getConnection(dataSource));
DatabaseConfig config = connection.getConfig();
config.setProperty("http://www.dbunit.org/properties/datatypeFactory", new MySqlDataTypeFactory());
//trunkTables(connection);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL url = classLoader.getResource(INSERT_TBL);
if (url == null) {
classLoader = ClassLoader.getSystemClassLoader();
url = classLoader.getResource(INSERT_TBL);
}
IDataSet dateSetInsert = new FlatXmlDataSetBuilder().build(new FileInputStream(url.getFile()));
DatabaseOperation.CLEAN_INSERT.execute(connection, dateSetInsert);
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
if (connection != null) {
connection.close();
}
}
}
@After
public void tearDown() throws Exception {
IDatabaseConnection connection = null;
try {
connection = new DatabaseConnection(DataSourceUtils.getConnection(dataSource));
DatabaseConfig config = connection.getConfig();
config.setProperty("http://www.dbunit.org/properties/datatypeFactory", new MySqlDataTypeFactory());
//trunkTables(connection);
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
if (connection != null) {
connection.close();
}
}
}
private void trunkTables(IDatabaseConnection connection) throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL url = classLoader.getResource(DEL_TBL);
if (url == null) {
classLoader = ClassLoader.getSystemClassLoader();
url = classLoader.getResource(DEL_TBL);
}
XmlUtil xmlUtil = new XmlUtil();
List tablesList = xmlUtil.getCleanTables(url.getFile());
Iterator it = tablesList.iterator();
while (it.hasNext()) {
TableBean tBean = (TableBean) it.next();
IDataSet dataSetDel = new DefaultDataSet(new DefaultTable(tBean.getTableName()));
DatabaseOperation.DELETE_ALL.execute(connection, dataSetDel);
}
}
@Test
@Rollback(false)
public void testGetAllStudent() throws Exception {
List<StudentVO> stdList = new ArrayList<StudentVO>();
stdList = stdService.getAllStudent();
assertEquals(5, stdList.size());
}
}
- 该测试方法每次都清空t_student表
- 往t_student表里注入5条数据
- 运行业务方法getAllStudent
- 比较getAllStudent方法返回的list里的size是否为5
- 清空注入的数据(也可不用去清空)
然后我们在eclipse里用junit来运行我们这个测试类吧。
我们现在用我们的单元测试用数据库帐号连入我们的数据库,查询t_student表
我们往该表中手动插入一条数据
再重新运行一遍我们的单元测试
测试结果还是成功,再重新连入我们单元测试用数据库实例查询t_student表,发觉还是5条记录,说明我们的框架达到了我们的目标。
四、将ant与我们的单元测试框架连接起来并生成单元测试报告
先来看一下我们的nightly building,即每天次日的零晨将要生成的单元测试与打包布署的流程吧
(需要ant1.8及以上版本运行)
然后下面给出build.xml文件(需要ant1.8及以上版本运行)(结合了maven的依赖库机制)
build.properties文件
[plain] view plain copy print ?
- # ant
- appName=myssh2
- webAppName=myssh2
- webAppQAName=myssh2-UT
- local.dir=C:/eclipsespace/${appName}
- src.dir=${local.dir}/src/main/java
- test.src.dir=${local.dir}/test/main/java
- dist.dir=${local.dir}/dist
- report.dir=${local.dir}/report
- webroot.dir=${local.dir}/src/main/webapp
- lib.dir=${local.dir}/lib
- ext-lib.dir=${local.dir}/ext-lib
- classes.dir=${webroot.dir}/WEB-INF/classes
- resources.dir=${local.dir}/src/main/resources
# ant
appName=myssh2
webAppName=myssh2
webAppQAName=myssh2-UT
local.dir=C:/eclipsespace/${appName}
src.dir=${local.dir}/src/main/java
test.src.dir=${local.dir}/test/main/java
dist.dir=${local.dir}/dist
report.dir=${local.dir}/report
webroot.dir=${local.dir}/src/main/webapp
lib.dir=${local.dir}/lib
ext-lib.dir=${local.dir}/ext-lib
classes.dir=${webroot.dir}/WEB-INF/classes
resources.dir=${local.dir}/src/main/resources
build.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project name="myssh2" default="buildwar" xmlns:artifact="urn:maven-artifact-ant">
<property file="build.properties" />
<property name="classes.dir" value="${dist.dir}/${webAppName}/WEB-INF/classes" />
<path id="maven-ant-tasks.classpath" path="C:/ant/lib/maven-ant-tasks-2.1.3.jar" />
<typedef resource="org/apache/maven/artifact/ant/antlib.xml" uri="urn:maven-artifact-ant" classpathref="maven-ant-tasks.classpath" />
<artifact:pom id="maven.project" file="pom.xml" />
<artifact:dependencies filesetId="deps.fileset.compile" useScope="compile">
<!--<pom file="pom.xml"/>-->
<pom refid="maven.project" />
</artifact:dependencies>
<path id="compile.classpath">
<fileset dir="${lib.dir}">
<include name="*.jar" />
</fileset>
</path>
<target name="clean" description="Delete old build and dist directories">
<delete dir="${dist.dir}" />
<delete dir="${report.dir}" />
<mkdir dir="${report.dir}" />
<mkdir dir="${dist.dir}" />
<!-- create war structure for production env-->
<mkdir dir="${dist.dir}/${webAppName}" />
<mkdir dir="${dist.dir}/${webAppName}/WEB-INF" />
<mkdir dir="${dist.dir}/${webAppName}/WEB-INF/lib" />
<mkdir dir="${dist.dir}/${webAppName}/WEB-INF/classes" />
<mkdir dir="${dist.dir}/${webAppName}/css" />
<mkdir dir="${dist.dir}/${webAppName}/images" />
<mkdir dir="${dist.dir}/${webAppName}/jsp" />
<!-- create war structure for qa env -->
<mkdir dir="${dist.dir}/${webAppQAName}" />
<mkdir dir="${dist.dir}/${webAppQAName}/WEB-INF" />
<mkdir dir="${dist.dir}/${webAppQAName}/WEB-INF/lib" />
<mkdir dir="${dist.dir}/${webAppQAName}/WEB-INF/classes" />
<mkdir dir="${dist.dir}/${webAppQAName}/WEB-INF/classes/org/sky/ssh/ut/ds" />
<mkdir dir="${dist.dir}/${webAppQAName}/WEB-INF/classes/org/sky/ssh/ut/xmldata/student" />
<mkdir dir="${dist.dir}/${webAppQAName}/css" />
<mkdir dir="${dist.dir}/${webAppQAName}/images" />
<mkdir dir="${dist.dir}/${webAppQAName}/jsp" />
</target>
<target name="download-libs" depends="clean">
<copy todir="${lib.dir}">
<fileset refid="deps.fileset.compile" />
<mapper type="flatten" />
</copy>
</target>
<target name="compile" description="Compile java sources" depends="download-libs">
<!-- compile main class -->
<javac debug="true" destdir="${dist.dir}/${webAppName}/WEB-INF/classes" includeAntRuntime="false" srcdir="${src.dir}">
<classpath refid="compile.classpath" />
</javac>
<copy todir="${dist.dir}/${webAppName}/WEB-INF/lib">
<fileset dir="${lib.dir}">
<include name="*.jar" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppName}/WEB-INF/classes">
<fileset dir="${resources.dir}">
<include name="**/*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppName}/css">
<fileset dir="${webroot.dir}/css">
<include name="**/*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppName}/images">
<fileset dir="${webroot.dir}/images">
<include name="**/*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppName}/jsp">
<fileset dir="${webroot.dir}/jsp">
<include name="**/*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppName}">
<fileset dir="${webroot.dir}">
<include name="*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppName}/WEB-INF">
<fileset dir="${webroot.dir}/WEB-INF">
<include name="*.*" />
</fileset>
</copy>
</target>
<target name="compileQA" description="Compile java sources" depends="compile">
<!-- compile main class -->
<javac debug="true" destdir="${dist.dir}/${webAppQAName}/WEB-INF/classes" includeAntRuntime="false" srcdir="${src.dir}">
<classpath refid="compile.classpath" />
</javac>
<javac debug="true" destdir="${dist.dir}/${webAppQAName}/WEB-INF/classes" includeAntRuntime="false" srcdir="${test.src.dir}">
<classpath refid="compile.classpath" />
</javac>
<copy todir="${dist.dir}/${webAppQAName}/WEB-INF/lib">
<fileset dir="${lib.dir}">
<include name="*.jar" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppQAName}/WEB-INF/classes">
<fileset dir="${resources.dir}">
<include name="**/*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppQAName}/css">
<fileset dir="${webroot.dir}/css">
<include name="**/*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppQAName}/images">
<fileset dir="${webroot.dir}/images">
<include name="**/*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppQAName}/jsp">
<fileset dir="${webroot.dir}/jsp">
<include name="**/*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppQAName}">
<fileset dir="${webroot.dir}">
<include name="*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppQAName}/WEB-INF">
<fileset dir="${webroot.dir}/WEB-INF">
<include name="*.*" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppQAName}/WEB-INF/classes/org/sky/ssh/ut/ds">
<fileset dir="${test.src.dir}/org/sky/ssh/ut/ds">
<include name="*.xml" />
</fileset>
</copy>
<copy todir="${dist.dir}/${webAppQAName}/WEB-INF/classes/org/sky/ssh/ut/xmldata/student">
<fileset dir="${test.src.dir}/org/sky/ssh/ut/xmldata/student">
<include name="*.xml" />
</fileset>
</copy>
<antcall target="junitreport">
</antcall>
</target>
<target name="buildwar" depends="compileQA">
<war warfile="${dist.dir}/${webAppName}.war">
<fileset dir="${dist.dir}/${webAppName}" />
</war>
</target>
<target name="junitreport">
<junit printsummary="on" haltonfailure="false" failureproperty="tests.failed" showoutput="true">
<classpath>
<pathelement path="${dist.dir}/${webAppQAName}/WEB-INF/classes" />
<fileset dir="${lib.dir}">
<include name="*.jar" />
</fileset>
<fileset dir="${ext-lib.dir}">
<include name="*.jar" />
</fileset>
</classpath>
<formatter type="xml" />
<batchtest todir="${report.dir}">
<fileset dir="${dist.dir}/${webAppQAName}/WEB-INF/classes">
<include name="org/sky/ssh/ut/Test*.*" />
</fileset>
</batchtest>
</junit>
<junitreport todir="${report.dir}">
<fileset dir="${report.dir}">
<include name="TEST-*.xml" />
</fileset>
<report format="frames" todir="report" />
</junitreport>
<fail if="tests.failed">
---------------------------------------------------------
One or more tests failed, check the report for detail...
---------------------------------------------------------
</fail>
</target>
</project>
对照着上面的build的流程图,很容易看懂
打开一个command窗口,进入到我们的工程的根目录下,设置好ANT_HOME并将%ANT_HOME%\bin目录加入到path中去,然后在工程的根据目录下运行ant,就能看到打包和运行unit test的效果了。
build完后可以在工程的根目录下找到一个report目录,打开后里面有一堆的html文件
双击index.htm这个文件查看单元测试报告
我们在windows的资源管理器中打开我们的工程,在根目录下有一个dist目录,打开这个目录,我们会开到两个目录与一个.war文件,它们分别是:
其中myssh2-UT是专门用来run unit test的,而myssh2是可以用于发布到production environment的,我们打开myssh2.war这个包,我们可以看到,由于这个是正确布署的war,因此里面是不能够含有unit test的相关类与方法的,完全按照上述的打包流程图来做的。
结束今天的教程!!!