`
wangshiyang
  • 浏览: 71805 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

通向架构师的道路(第二十五天)SSH的单元测试与dbunit的整合

 
阅读更多



一、前言

在二十三天中我们介绍了使用maven来下载工程的依赖库文件,用ant来进行war包的建立。今天我们在这个基础上将使用junit+dbunit来进行带有单元测试报告的框架的架构。

目标:

  1. 每次打包之前自动进行单元测试并生成单元测试报告
  2. 生成要布署的打包文件即war包
  3. 单元测试的代码不能够被打在正式的要布署的war包内,单元测试仅用于unit test用
  4. 使用模拟数据对dao层进行测试,使得dao方法的测试结果可被预料

二、Junit+Ant生成的单元测试报告




上面是一份junit生成的测试报告,它可以与ant任务一起运行然后自动生成这么一份html的测试报告,要生成这样的一份junit test report我们需要调用ant任务中的<junitreport>这个task,示例代码如下:
<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,因此我们每天的打包要干的事情应该是:
  1. 程序员check in代码时必须把相关的unit test也check in源码服务器
  2. 次日的零晨由持续集成构件如:cruisecontrol自动根据设好的schedule把所有的源码服务器的代码进行编译
  3. 运行单元测试
  4. 生成报告
  5. 打包布署到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

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

<?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

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来运行,运行后直接出错抛出:

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这个选项栏


  1. 单击user Entries
  2. 单击Advanced按钮
  3. 在弹出框中选择Add Folders
  4. 点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

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

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

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文件

<?xml version="1.0" encoding="UTF-8"?>
<Tables>
	<table>t_student</table>
</Tables>


test_insert_table.xml文件

<?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());
	}
}
  1. 该测试方法每次都清空t_student表
  2. 往t_student表里注入5条数据
  3. 运行业务方法getAllStudent
  4. 比较getAllStudent方法返回的list里的size是否为5
  5. 清空注入的数据(也可不用去清空)
然后我们在eclipse里用junit来运行我们这个测试类吧。

我们现在用我们的单元测试用数据库帐号连入我们的数据库,查询t_student表

我们往该表中手动插入一条数据
再重新运行一遍我们的单元测试
测试结果还是成功,再重新连入我们单元测试用数据库实例查询t_student表,发觉还是5条记录,说明我们的框架达到了我们的目标。



四、将ant与我们的单元测试框架连接起来并生成单元测试报告

先来看一下我们的nightly building,即每天次日的零晨将要生成的单元测试与打包布署的流程吧


(需要ant1.8及以上版本运行)

然后下面给出build.xml文件(需要ant1.8及以上版本运行)(结合了maven的依赖库机制)

build.properties文件

# 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的相关类与方法的,完全按照上述的打包流程图来做的。

结束今天的教程!!!

分享到:
评论

相关推荐

    通向架构师的道路(第二十五天)SSH的单元测试与dbunit的整合.docx

    SSH单元测试与DbUnit整合 在软件开发过程中,单元测试是一个非常重要的步骤,它可以确保代码的正确性和可靠性。今天,我们将讨论如何使用JUnit和DbUnit进行单元测试,并将其与SSH整合。 SSH单元测试 在SSH项目中...

    通向架构师的道路(第二十五天)SSH的单元测试与dbunit的整合的例子

    在IT行业中,尤其是在软件开发领域,单元测试是保证代码质量、可维护性和可扩展性的重要...在实际项目中,一个良好的测试策略能够帮助我们更快地发现和修复问题,提高开发效率,这也是通向架构师道路的关键步骤之一。

    通向架构师的道路.rar

    8. **通向架构师的道路(第二十五天)SSH的单元测试与dbunit的整合.docx** 单元测试是保证代码质量的重要手段,而DbUnit则支持数据库的单元测试。文档可能讲述了如何使用JUnit、Mockito等工具,结合DbUnit进行SSH...

    通向架构师的道路

    【通向架构师的道路】是一篇详尽的指南,旨在帮助初学者逐步迈进架构师的领域。该文从基础架构的搭建开始,逐渐深入到高级技术应用和优化,覆盖了多个关键的技术点,如服务器整合、性能调优、权限系统设计、Web服务...

    3.4.7-卡尔曼滤波与运动估计算法介绍和使用 STM32串口通信 openmv+STM32串口通信 openmv串口通信openmv识别物体 openmv神经网络训练 openmv数字识.md

    3.4.7-卡尔曼滤波与运动估计算法介绍和使用 STM32串口通信 openmv+STM32串口通信 openmv串口通信openmv识别物体 openmv神经网络训练 openmv数字识

    【MATLAB编程】MATLAB高级编程技巧全解析:数据结构、高效编程、可视化与并行计算

    内容概要:本文档详细介绍了MATLAB的高级编程技巧,涵盖高级数据结构与操作、高效编程与优化、高级可视化技术、并行计算与GPU编程、代码调试与性能分析以及高级算法与数值方法六个主要部分。具体内容包括细胞数组和结构体的创建与使用、面向对象编程、向量化

    一个用于设计和测试单点海洋系泊的工具-MATLAB

    用于设计和测试受洋流和风影响的单点海洋系泊设备。提供的数据库(可以添加到)将允许您在典型的当前条件下构建系泊并对其进行测试,或者通过电流剖面的时间序列迫使其生成系泊形状和组件位置的时间序列。该程序为地下和地面系泊提供了解决方案,甚至可以预测地面系泊何时被淹没。将溶液上下翻转,它还模拟了拖曳体,在拖曳体中,一个负浮力(重)体被拖在一艘移动的船后面(有一个立管下沉器)。如果聚焦电流剖面随时间变化(即来自ADCP),则会保存/访问系泊组件位置和形状的动画(电影)或时间(序列)历史。系泊装置可以保存和取回

    C++ 无锁队列,目前测试10线程读,10线程写无误

    基于C++的无锁队列,有信号量,可以阻塞读、写线程,目前测试10线程读,10线程写是没有问题的

    混合动力汽车拥堵路况下基于ECMS算法的节能动力总成控制研究与实现

    内容概要:本文详细介绍了混合动力汽车在交通拥堵情况下采用等效燃油消耗最小策略(ECMS)进行节能控制的研究与实现。通过MATLAB代码展示了如何模拟前车速度、跟车策略以及能量管理系统的工作原理。文中提到,ECMS算法能够根据实时速度、加速度和电池状态等因素动态调整发动机和电动机之间的能量分配,从而达到最佳的燃油经济性和排放性能。此外,文章还讨论了安全距离模型、动能回收机制以及应对突发情况的具体措施,如急减速时的能量管理和电池温度保护等。 适合人群:从事汽车工程、自动化控制领域的研究人员和技术人员,尤其是关注混合动力汽车节能技术的专业人士。 使用场景及目标:适用于希望深入了解混合动力汽车在复杂城市交通环境中的节能控制策略及其具体实施方法的人群。目标是在理论层面掌握ECMS算法的核心思想,并能够在实践中应用相关技术和工具进行验证。 其他说明:文章不仅提供了详细的代码示例,还分享了一些实用的操作技巧和经验教训,帮助读者更好地理解和运用这一先进技术。

    环形队列的一种实现方式

    环形队列的一种实现方式

    2005-2019年各地级市绿色专利申请量数据

    2005-2019年各地级市绿色专利申请量数据 1、时间2005-2019年 2、来源:国家知识产权j 3、指标:省份、城市、年份、绿色发明专利申请量、绿色实用新型专利申请量 4、范围:360+地级市

    (源码)基于C语言的WebP图片编码解码库.zip

    # 基于C语言的WebP图片编码解码库 ## 项目简介 本项目是一个基于C语言的WebP图片编码解码库。WebP是一种现代开源图像格式,支持无损和有损压缩,在保持图像质量的同时能显著减少图像文件大小,加快传输速度。此库实现了WebP格式的核心编码解码功能,还支持动画WebP图像的编码和组装,以及颜色空间转换、图像缩放等辅助功能,适用于网页开发、图像处理软件、视频编辑工具等多种应用场景。 ## 项目的主要特性和功能 1. 高性能采用优化算法和指令集(如SSE2、NEON、MIPS等)加速编码解码过程。 2. 灵活性支持多种图像格式(如RGB、YUV)和颜色空间转换。 3. 支持动画可编码和组装WebP格式的动画图像。 4. 内存管理提供安全的内存分配和释放函数,防止内存泄漏。 5. 错误处理具备错误报告和处理机制,确保程序的健壮性。 6. 核心功能实现图像分析、预测、变换、量化、反变换等编码解码步骤,并提供进度报告机制。 ## 安装使用步骤

    基于电影知识图谱和微信小程序的智能问答系统新版源码+说明.zip

    本资源是《基于电影知识图谱和微信小程序的智能问答系统新版源码+说明.zip》,专为计算机科学领域的学习者设计,融合了人工智能、数据管理和前端应用开发的先进技术。通过构建电影知识图谱,该系统能够深度理解和处理用户的电影相关查询,提供精准且丰富的答案。微信小程序作为前端交互平台,以其便捷性和广泛覆盖性,让用户随时随地享受智能化的电影信息服务。此资源不仅是课程设计和毕业设计的理想选择,也为开发者提供了实践前沿技术的机会,助力提升编程能力和项目经验。请务必用于学习和研究目的,不得用于商业用途。

    (源码)基于TensorFlow的GAN图像生成项目SteGANography.zip

    # 基于TensorFlow的GAN图像生成项目SteGANography ## 项目简介 本项目是一个基于TensorFlow深度学习框架的图像生成项目,主要利用生成对抗网络(GAN)进行图像混淆与恢复的研究。项目名称中的“Ste”代表Steganography(隐写术),是一种隐藏信息的技术。本项目的目的是利用神经网络将信息嵌入图像中,然后再恢复出来。这涉及到图像生成、加密和解密的过程。 ## 主要功能及特点 利用GAN生成混淆图像。 通过对混淆图像进行解码恢复原始图像。 包含Alice、Bob和Eve三个网络部分,分别负责生成、处理和识别图像。 提供了可视化的功能,能够绘制关于训练迭代与位错误之间的关系图。 可配置的训练参数,包括学习率、迭代次数等。 提供了模型保存和恢复的机制,便于训练中断后继续训练或在不同任务间迁移模型。 ## 安装与运行 ### 环境依赖 Python 3.x

    (源码)基于Arduino的环境监测系统.zip

    # 基于Arduino的环境监测系统 ## 项目简介 本项目是一个基于Arduino的环境监测系统,主要用于收集并保存环境数据,如温度、湿度、降雨量、气体值和风速等。通过使用Arduino平台和多种传感器,实现数据的实时采集、处理和存储。 ## 项目的主要特性和功能 1. 数据采集通过Arduino板连接多种环境传感器,实时采集环境数据。 2. 数据处理对采集的数据进行本地处理,如温度湿度的转换等。 3. 数据存储将处理后的数据保存到SD卡或其他存储设备中,以便于后续分析。 4. 数据传输通过串行通信或无线通信模块将数据发送到计算机或其他设备。 ## 安装使用步骤 1. 硬件准备准备Arduino板、环境传感器(如DHT温湿度传感器)、SD卡模块等硬件。 2. 软件准备安装Arduino IDE,并下载项目代码。 3. 传感器连接将传感器连接到Arduino板的相应引脚。 4. 代码上传将项目代码上传到Arduino板。

    100kw模块式三相光伏并网逆变器方案:原理图、PCB、源码及元器件详解

    内容概要:本文详细介绍了100kw模块式三相光伏并网逆变器的整体设计方案,涵盖功率接口板、主控DSP板、驱动扩展板及其逆变器并联仿真的各个方面。首先,文章阐述了功率接口板的原理图和PCB设计,解释了各个元件的作用及其选择依据。接着,重点讲解了主控DSP板的原理图、元器件明细表及核心代码,特别是PID控制算法的应用。然后,探讨了驱动扩展板的设计要点,包括驱动芯片的选择和PCB布局技巧。最后,分析了逆变器并联仿真文件,讨论了环流抑制算法及其效果。 适合人群:具备一定电子工程和嵌入式系统基础知识的专业人士,尤其是从事光伏逆变器及相关电力电子设备的研发工程师和技术爱好者。 使用场景及目标:①帮助读者深入了解100kw模块式三相光伏并网逆变器的工作原理和设计思路;②为实际项目开发提供详尽的技术参考资料,包括原理图、PCB设计、源码及元器件选择;③通过实例代码和仿真分析,提升读者解决复杂控制系统问题的能力。 其他说明:文中不仅提供了理论知识,还分享了许多实际设计中的经验和技巧,如PCB布线注意事项、元件选型标准、代码优化方法等,有助于读者更好地理解和应用所学内容。

    (源码)基于Python和OpenAI的微信智能聊天机器人.zip

    # 基于Python和OpenAI的微信智能聊天机器人 ## 项目简介 本项目是一个基于Python和OpenAI的微信智能聊天机器人,能够将微信打造成一个智能对话平台。通过集成OpenAI的ChatGPT模型,机器人可以进行智能对话,支持多轮会话上下文记忆、语音识别、图片生成等功能。此外,项目还支持多种插件扩展,如多角色切换、文字冒险游戏、敏感词过滤等,极大地丰富了用户的互动体验。 ## 主要特性和功能 多端部署支持个人微信、微信公众号和企业微信应用等多种部署方式。 智能对话支持私聊及群聊的智能回复,支持多轮会话上下文记忆,支持GPT3、GPT3.5、GPT4模型。 语音识别支持语音消息的识别与回复,支持Azure、Baidu、Google、OpenAI等多种语音模型。 图片生成支持图片生成和图生图功能,支持DALLE、Stable Diffusion、Replicate等模型。

    图表组件(柱状图、折线图、饼图、雷达图...)微信小程序源码.zip

    微信小程序图表组件源码简介 这份资源是精心整理的微信小程序图表组件源码包,涵盖了柱状图、折线图、饼图、雷达图等多种常见且实用的图表类型。在当今数据可视化盛行的时代,无论是商业数据分析、项目进度展示,还是日常信息统计,这些图表都起着关键作用。 对于微信小程序开发者而言,这无疑是一份极具价值的学习宝藏。它提供了现成的图表实现代码,能帮助开发者深入理解不同图表在小程序中的构建逻辑与交互方式,快速掌握如何将数据以直观的图表形式呈现给用户,从而提升小程序的用户体验与功能性。通过研究这些源码,开发者可以学习到图表绘制的技巧、数据处理的方法以及与小程序其他组件协同工作的方式,为开发更复杂、更专业的小程序应用奠定坚实基础,加速开发进程,少走弯路,进而打造出功能完备、界面美观且数据展示清晰的优质小程序产品,满足用户多样化的数据可视化需求。总之,这是一份不可多得的学习资源,助力开发者在微信小程序开发领域不断进步。

    幼儿园招生报名小程序源码(幼儿家长可以填写幼儿的基本信息,住址信息,监护人信息等资料(可自定义设置), 园方人员根据资料可以做预先审核,并提示用户修改完善资料,并可查看和导出名单).zip

    《幼儿园招生报名小程序源码简介》 本资源是一份极具实用价值的幼儿园招生报名小程序源码。它为幼儿园的招生工作提供了高效便捷的解决方案,同时也方便家长进行报名操作。 在功能方面,幼儿家长能够通过该小程序填写丰富的信息,涵盖幼儿的基本信息、住址信息以及监护人信息等各类必要资料,且这些资料可根据实际需求进行灵活的自定义设置,满足不同幼儿园的个性化要求。园方人员则拥有强大的管理权限,可依据家长提交的资料进行预先审核,若发现资料不完善或存在问题,能及时提示用户修改,确保信息的准确性和完整性。此外,园方还能方便地查看所有报名名单,并支持将名单导出,便于后续的整理和统计工作。 需要强调的是,此资源仅为学习资源,旨在帮助开发者学习和研究相关技术,不应用于商业用途。

Global site tag (gtag.js) - Google Analytics