近期自己折腾自己,放着正统的maven + junit不用,却准备用ant + ivy 替代maven做依赖管理,用testng替代junit做单元测试。
现在要做的工作,其实很简单,就是ant的脚本中,搞定相关的target: 编译,运行单元测试。
需要的步骤大体如下:
1. ivy 做依赖解析,得到所有依赖的jar包,以便生成编译源码需要的classpath路径
这里很重要的一点,是需要区分开编译正常代码的classpath和编译测试代码的classpath,因为通常情况下testcase需要一些特殊的依赖如juni,testng之类的测试框架,easymock,jmock之类的mock工具。
2. 编译代码和测试案例
3. 运行testng 来执行testcase
分别来看三者的实现。
1) ivy解析依赖
ant代码如下:
<
target
name
="ivy.resolve"
description
="--> resolve project dependencies by ivy"
>
< echo > resolve dependencies of project "${ant.project.name}" by ivy </ echo >
< ivy:resolve />
< ivy:cachefileset setid ="ivy.cachefileset.compile" type ="jar,bundle" conf ="compile" />
< ivy:cachefileset setid ="ivy.cachefileset.test" type ="jar,bundle" conf ="test" />
</ target >
< echo > resolve dependencies of project "${ant.project.name}" by ivy </ echo >
< ivy:resolve />
< ivy:cachefileset setid ="ivy.cachefileset.compile" type ="jar,bundle" conf ="compile" />
< ivy:cachefileset setid ="ivy.cachefileset.test" type ="jar,bundle" conf ="test" />
</ target >
用到了ivy标准的resolve 任务,然后再用cachefileset来获取需要的文件列表,以备后面使用。注意type要写成"jar,bundle",因此目前的依赖包虽然扩张名都是jar,但是它的ivy类型定义确有可能为bundle(都是OSGI惹得祸)。还有上面用了两次cachefileset任务,conf不同。
2) 编译代码和测试案例
<
target
name
="compile.compile"
depends
="ivy.resolve"
>
< echo > compile project ${ant.project.name} </ echo >
< compile_source_main />
< compile_source_test />
</ target >
< macrodef name ="compile_source_main" >
< sequential >
< echo > compile java classes in project ${ant.project.name} </ echo >
< echo > classpath is : ${project.classpath.compile.ivy.lib} </ echo >
< delete dir ="${dir.target.bin.main}" />
< mkdir dir ="${dir.target.bin.main}" />
< javac debug ="true" srcdir ="${dir.src.main.java}" destdir ="${dir.target.bin.main}" target ="1.5" includeAntRuntime ="false" >
< classpath >
< pathelement location ="${dir.src.main.resources}" />
< fileset refid ="ivy.cachefileset.compile" />
</ classpath >
</ javac >
</ sequential >
</ macrodef >
< macrodef name ="compile_source_test" >
< sequential >
< echo > compile java testcase in project ${ant.project.name} </ echo >
< echo > classpath is : ${project.classpath.test.ivy.lib} </ echo >
< delete dir ="${dir.target.bin.test}" />
< mkdir dir ="${dir.target.bin.test}" />
< javac debug ="true" srcdir ="${dir.src.test.java}" destdir ="${dir.target.bin.test}" target ="1.5" includeAntRuntime ="false" >
< classpath >
< pathelement location ="${dir.src.test.resources}" />
< pathelement location ="${dir.src.main.resources}" />
< pathelement location ="${dir.target.bin.main}" />
< fileset refid ="ivy.cachefileset.test" />
</ classpath >
</ javac >
</ sequential >
</ macrodef >
< echo > compile project ${ant.project.name} </ echo >
< compile_source_main />
< compile_source_test />
</ target >
< macrodef name ="compile_source_main" >
< sequential >
< echo > compile java classes in project ${ant.project.name} </ echo >
< echo > classpath is : ${project.classpath.compile.ivy.lib} </ echo >
< delete dir ="${dir.target.bin.main}" />
< mkdir dir ="${dir.target.bin.main}" />
< javac debug ="true" srcdir ="${dir.src.main.java}" destdir ="${dir.target.bin.main}" target ="1.5" includeAntRuntime ="false" >
< classpath >
< pathelement location ="${dir.src.main.resources}" />
< fileset refid ="ivy.cachefileset.compile" />
</ classpath >
</ javac >
</ sequential >
</ macrodef >
< macrodef name ="compile_source_test" >
< sequential >
< echo > compile java testcase in project ${ant.project.name} </ echo >
< echo > classpath is : ${project.classpath.test.ivy.lib} </ echo >
< delete dir ="${dir.target.bin.test}" />
< mkdir dir ="${dir.target.bin.test}" />
< javac debug ="true" srcdir ="${dir.src.test.java}" destdir ="${dir.target.bin.test}" target ="1.5" includeAntRuntime ="false" >
< classpath >
< pathelement location ="${dir.src.test.resources}" />
< pathelement location ="${dir.src.main.resources}" />
< pathelement location ="${dir.target.bin.main}" />
< fileset refid ="ivy.cachefileset.test" />
</ classpath >
</ javac >
</ sequential >
</ macrodef >
注意这里用到classpath时,是在上面得到的ivy cachefileset的基础上,增加其他路径才得到最终的classpath。曾经在这里折腾了不少时间,因为开始是用ivy的cacheclasspath任务直接拿到一个classpath,然后在这里发现单有这个classpath是不够的。可是又没有找到如何从一个classpath生成一个更多内容的classpath的方法(郁闷,ant里面的classpath似乎不支持这种classpath=***+***+classpath的算法,或者是我笨没有找到)。最后只好改用cachefileset来获取fileset,然后自己增加其他路径。典型如编译测试案例时,必须将前面编译好的class作为classpath的一部分增加。从这种角度讲,ivy的cacheclasspath任务是用处不大的,实用的是cachefileset任务。
3) 运行testng
首先需要初始化testng,引入testng的任务。
<
target
name
="testng.init"
depends
="ivy.resolve"
>
< taskdef resource ="testngtasks" >
< classpath >
< fileset refid ="ivy.cachefileset.test" />
</ classpath >
</ taskdef >
</ target >
< taskdef resource ="testngtasks" >
< classpath >
< fileset refid ="ivy.cachefileset.test" />
</ classpath >
</ taskdef >
</ target >
在具体执行testng时,有两种选择:
1. 通过testng.xml指定具体的测试案例
应该说testng对此有非常强大而富有弹性的支持,通过testng.xml可以指定不同的package,class,可以指定exclude,可以分组,还有其他高级特性。
2. 运行所有案例
使用testng.xml文件的前提是项目有提供testng.xml文件,对于一些简单的项目,可能只是简单的希望执行所有testcase,因此就需要在运行检测testng.xml文件存在与否。
<
target
name
="testng.test"
depends
="testng.init"
>
< if >
< resourceexists >
< file file ="${dir.src.test.java}/testng.xml" />
</ resourceexists >
< then >
< run_testng_with_xml />
</ then >
< else >
< run_testng_without_xml />
</ else >
</ if >
</ target >
< if >
< resourceexists >
< file file ="${dir.src.test.java}/testng.xml" />
</ resourceexists >
< then >
< run_testng_with_xml />
</ then >
< else >
< run_testng_without_xml />
</ else >
</ if >
</ target >
testng.xml存在时,通过xmlfileset来调用testng任务:
<
macrodef
name
="run_testng_with_xml"
>
< sequential >
< echo > run testng to test project "${ant.project.name}". </ echo >
< echo > found ${dir.src.test.java}/testng.xml, use it to run testng. </ echo >
< delete dir ="${dir.target.testng.testoutput}" />
< testng outputDir ="${dir.target.testng.testoutput}" haltOnfailure ="true" >
< xmlfileset dir ="${dir.src.test.java}" includes ="testng.xml" />
< classpath >
< pathelement location ="${dir.src.test.resources}" />
< pathelement location ="${dir.src.main.resources}" />
< pathelement location ="${dir.target.bin.test}" />
< pathelement location ="${dir.target.bin.main}" />
< fileset refid ="ivy.cachefileset.test" />
</ classpath >
</ testng >
</ sequential >
</ macrodef >
< sequential >
< echo > run testng to test project "${ant.project.name}". </ echo >
< echo > found ${dir.src.test.java}/testng.xml, use it to run testng. </ echo >
< delete dir ="${dir.target.testng.testoutput}" />
< testng outputDir ="${dir.target.testng.testoutput}" haltOnfailure ="true" >
< xmlfileset dir ="${dir.src.test.java}" includes ="testng.xml" />
< classpath >
< pathelement location ="${dir.src.test.resources}" />
< pathelement location ="${dir.src.main.resources}" />
< pathelement location ="${dir.target.bin.test}" />
< pathelement location ="${dir.target.bin.main}" />
< fileset refid ="ivy.cachefileset.test" />
</ classpath >
</ testng >
</ sequential >
</ macrodef >
testng.xml不存在时,通过classfileset来指定需要执行的class:
<
macrodef
name
="run_testng_without_xml"
>
< sequential >
< if >
< resourcecount when ="greater" count ="0" >
< fileset dir ="${dir.target.bin.test}" includes ="**/*.class" />
</ resourcecount >
< then >
< echo > run testng to test project "${ant.project.name}". </ echo >
< echo > ${dir.src.test.java}/testng.xml not found, default to run all the testcase. </ echo >
< delete dir ="${dir.target.testng.testoutput}" />
< testng outputDir ="${dir.target.testng.testoutput}" haltOnfailure ="true" >
< classfileset dir ="${dir.target.bin.test}" includes ="**/*.class" />
< classpath >
< pathelement location ="${dir.src.test.resources}" />
< pathelement location ="${dir.src.main.resources}" />
< pathelement location ="${dir.target.bin.test}" />
< pathelement location ="${dir.target.bin.main}" />
< fileset refid ="ivy.cachefileset.test" />
</ classpath >
</ testng >
</ then >
< else >
< echo > no testcase exist in "${dir.target.bin.test}", nothing to do for testng. </ echo >
</ else >
</ if >
</ sequential >
</ macrodef >
< sequential >
< if >
< resourcecount when ="greater" count ="0" >
< fileset dir ="${dir.target.bin.test}" includes ="**/*.class" />
</ resourcecount >
< then >
< echo > run testng to test project "${ant.project.name}". </ echo >
< echo > ${dir.src.test.java}/testng.xml not found, default to run all the testcase. </ echo >
< delete dir ="${dir.target.testng.testoutput}" />
< testng outputDir ="${dir.target.testng.testoutput}" haltOnfailure ="true" >
< classfileset dir ="${dir.target.bin.test}" includes ="**/*.class" />
< classpath >
< pathelement location ="${dir.src.test.resources}" />
< pathelement location ="${dir.src.main.resources}" />
< pathelement location ="${dir.target.bin.test}" />
< pathelement location ="${dir.target.bin.main}" />
< fileset refid ="ivy.cachefileset.test" />
</ classpath >
</ testng >
</ then >
< else >
< echo > no testcase exist in "${dir.target.bin.test}", nothing to do for testng. </ echo >
</ else >
</ if >
</ sequential >
</ macrodef >
注意这里有个检测,判断是否有测试案例存在,如果没有写测试案例,则跳过testng任务的执行,否则如果classfileset为空,testng即得不到testng.xml的输入,也得不到classfileset的输入,会直接报错的,因此需要避免因为没有测试案例导致test失败进而整个build都失败的情况。
OK,上述三板斧下去,基本ant + ivy + testng就可以完成整合,一起跑起来了。敲一个ant test下去,就可以依赖解析,编译,执行testcase的全套过程。过程比maven + junit复杂多了,主要是一切都要自己动手,不过完成之后的效果似乎还不错。上述的过程对于一般项目都是通用的,因此以后就可以偷懒了。