一、 TDD中单元测试的重要性
敏捷开发过程中TDD的开发方式是一种比较好的实践,而对于传统的开发方式中,开发人员一直习惯于先编写实现代码,然后再象征性的编写单元测试代码,甚至为了追求单元测试的覆盖率而不惜采用一些单元测试自动生成工具编写一些毫无意义的单元测试逻辑,这种单元测试显然对于提高产品质量无任何意义的,那么如何才能使单元测试发挥最大作用,有一种比较好的实践就是TDD,TDD有两层作用,一方面在测试代码的辅助下,开发人员可以快速的实现客户需求的功能。通过编写测试用例,对客户需求的功能进行分解,并进行系统设计。从使用角度对代码的设计通常更符合后期开发的需求。可测试的要求,对代码的内聚性的提高和复用都非常有益。另一方面,在测试代码的保护下,开发人员可以不断重构代码,提高代码的重用性,从而提高软件产品的质量。可见TDD实施的好坏可以影响产品的质量,我们应该使TDD贯穿软件开发的始终。
简单介绍了TDD的好处之后就进入到本文所关注的主题,本文所关注的是TDD的执行过程中开发人员如何简单快速的实现单元测试代码的编写这一环节, 这里有一种比较好的实践就是采用JUnit+Groovy+GMock实现单元测试代码的编写,可能大家会感觉又要学习一种语言,是的,但是这门语言(Groovy)是一种基于JVM的语言,语法通Java类似,所以对于Java程序员来说学习成本是很低的。至于JUnit和GMock都是一些辅助工具,学习成本更低,既然学习成本都很低,它对于传统的单元测试编写方式有哪些优势呢?我们下面就来进入基于JUnit+Groovy+GMock实现单元测试的实践中,通过实践比较我们会更容易的去体会他的优越之处。
Groovy语言的基本语法见(http://blog.csdn.net/hivon/archive/2009/06/10/4256296.aspx; http://groovy.codehaus.org/Documentation_CN);其中有较详细的介绍。对于使用其进行单元测试来说Groovy的语法不需要特别深入的掌握,大致了解即可。
另:关于更多TDD的介绍可以参考如下视频和文章的介绍:
1、 测试驱动开发(TDD)实战:http://www.infoq.com/cn/presentations/zxq-tdd
2、 三步实践测试驱动开发(上):http://www.infoq.com/cn/presentations/zj-tdd-1
3、 另外值得一提的是TDD中的Test不仅仅指的是单元测试的形式,还可能包括自动化测试等;如这篇文章中介绍的方式:使用Selenium和Castle进行测试驱动开发:http://www.infoq.com/cn/articles/Tutorial-TDD-Selenium
二、 用Groovy+JUnit+GMock实现单元测试
1.关于JUnit和Groovy
Groovy语言完全支持JUnit工具做Java代码的单元测试,Groovy自身也具有Mock对象的能力,不过该处我们不看Groovy的Mcok对象的功能,我们只列举一些例子去看看其与JUnit结合做单元测试的情况,至于在单元测试中需要Mock对象的情况我们交给GMock去处理。
下面就给出一个简单的例子去演示Groovy与JUnit结合做单元测试的情况:
import org.junit.Test
import static org.junit.Assert.assertEquals
class ArithmeticTest {
@Test
void additionIsWorking() {
assertEquals 4,2+2
}
@Test(expected=ArithmeticException)
void divideByZero() {
println 1/0
}
}
上面的例子和用Java做单元测试基本类似。另外,也可以像如下所示的方式去使用shouldFail方法去处理一些异常情况的覆盖:
class ArithmeticTest {
final shouldFail = new GroovyTestCase().&shouldFail
@Test
void divideByZero() {
shouldFail(ArithmeticException) {
println 1/0
}
}
}
我们的测试类包含两个测试用例,分别是additionIsWorking,另外一个是divideByZero;第二个测试用例验证当运行失败时是否抛出ArithmeticException异常。
关于Groovy结合JUnit的单元测试就介绍这一点,因为当熟悉一下Groovy语法之后,用Groovy和用Java结合JUnit做单元测试是没有任何区别的。
在用Java做单元测试的时候我们常常会使用一个模拟对象的框架叫做JMock,在Groovy结合JUnit做单元测试的时候也有一个功能类似于JMock的工具,就是GMock,下面我们去看一下GMock的一些常见用法。
2.关于GMock
Gmock是一种为Groovy语言设计的mock测试框架。通过Gmock设计的测试用例拥有简单的语法和可读性,所以你可以投入较少的时间去学习这个框架,它将为你俭省不少时间去编码;要使用Gmock只需要将Gmock的jar包丢入你的类路径下,并且保证你安装了JUnit。
这个文档所提到的内容是基于Groovy 1.6版本的0.8版本的Gmock框架的。
2.1 先看个简单的例子
废话不多说了,先上个例子看看:
import org.gmock.GMockTestCase
class LoaderTest extends GMockTestCase {
void testLoader(){
def mockLoader = mock()
mockLoader.load('key').returns('value')
play {
assertEquals "value",mockLoader.load('key')
}
}
}
Ø 首先使你的类需要继承GMockTestCase或者使用@WithGMock注解。
Ø 使用mock()方法创建mock对象。
Ø 在你的mock测试类中通过调用提供的方法设置期望的操作和值。
Ø 通过play闭包运行你所要测试的代码。
2.2 期望与运行方式
GMockTestCase类中提供了用于模拟对象的方法mock().默认情况下该类的实例会记录模拟对象的方法调用,并去检查是否符合期望。
被测试代码应该通过闭包的形式去执行(闭包是可以包含自由(未绑定)变量的代码块,获得了Groovy、Python等语言的支持)。示例如下:
void testBasic(){
def aMock = mock()
// 此处设置期望
play {
// 此处用于执行被验证的方法并进行验证
}
}
Gmock支持Java的强类型,mock()方法接受可选的类,这使得其甚至支持字节类型数据。例如:
File mockFile = mock(File)
2.3 强类型
Gmock完全支持Java的强类型。你不需要导入任何外部类库,GMock已经为您做好了一切。创建的虚拟对象可以被应用在纯Java的工程中,这使得Gmock成为Java测试中颇具优势的一种选择。
File mockFile = mock(File)
将构造方法和期望返回类型作为mock()方法参数使得强类型得到完美的支持。
File mockFile = mock(File,constructor("/a/path/file.txt"))
mockFile.getName().returns("file.txt")
play {
def file = new File("/a/path/file.txt")
assertEquals "file.txt",file.getName()
}
有时候你需要调用去模拟一些原始类型的对象。这种情况下需要使用回调构造器-invokeConstructor。
JavaLoader mock = mock(JavaLoader,invokeConstructor("loader"),constructor("name"))
这将会在模拟对象的过程中通过构造方法创建一个名为“loader”的JavaLoader。
2.4 模拟方法的使用
在调用模拟方法的时候方法的返回期望就被创建了,通过返回关键词来设置返回的值(即key-value的形式)。
def loader = mock()
loader.put("fruit").returns("apple")
play {
assertEquals "apple",loader.put("fruit")
}
异常可以通过一些高级关键字来设置。如下:
def loader = mock()
loader.put("throw exception").raises(new RuntimeException("an exception")) // or 'raises(RuntimeException,"an exception")'
play {
def message = shouldFail(RuntimeException) {
loader.put("throw exception")
}
assertEquals "an exception",message
}
2.5 属性使用
属性的使用需要使用如下的语法,需要通过Setters和Getters方法设置。
def loader = mock()
loader.name.set("a name")
loader.name.returns("a different name")
play {
loader.name = "a name"
assertEquals "a different name",loader.name
}
Ø loader.name.raises(RuntimeException)
Ø loader.name.set("invalid").raises(new RuntimeException())
Ø mockLoader.name.returns('a name').stub()
所以你可以在你的期望方法中写mockUrl.text.returns("some text")或者在你的代码中写mock.getText()。
2.6 静态调用
模拟静态方法调用以及属性调用和标准方法的调用也是十分相似,只是加上了static关键字。
def mockMath = mock(Math)
mockMath.static.random().returns(0.5)
play {
assertEquals 0.5,Math.random()
}
2.7局部mock
局部模拟是你能够从一个对象中mock出单独的一个方法。强迫自己从对象中模拟出一个方法通常被认为是一个不好的设计,但我们相信在动态环境中它会为你得到自动的方法注入。
在一个实际的对象中调用mock(object)将会返回这个对象一个模拟对象,你可以设置该对象能够使用的具体实现和期望。示例如下:
def controller = new SomeController()
def mockController = mock(controller)
mockController.params.returns([id: 3])
def mockRequest = mock()
mockController.request.returns(mockRequest)
或者你可以使用它的快捷方式:
def controller = new SomeController()
mock(controller).params.returns([id: 3])
def mockRequest = mock()
mock(controller).request.returns(mockRequest)
在Grails(一种新型 Web 开发框架,它将常见的 Spring 和 Hibernate 等 Java 技术与当前流行的约定优于配置等实践相结合。Grails 是用 Groovy 编写的,它可以提供与遗留 Java 代码的无缝集成,同时还可以加入脚本编制语言的灵活性和动态性)环境下它具有卓越的作用,让我们选择一个简单的标记库示例:
class FakeTagLib {
def hello = { attrs ->
out << "hello"
}
}
我们可以像这样mock出外部属性:
def tagLib = new FakeTagLib()
def mockTabLib = mock(tagLib)
def mockOut = mock()
mockTabLib.out.returns(mockOut)
mockOut << "hello"
play {
tagLib.hello()
}
2.8 构造方法的使用
def mockFile = mock(File,constructor("/a/path/file.txt"))
这将和new File("/a/path/file.txt")搭配,这个模拟出来的文件就可以被用于进一步的设置期望。
以下是完整的使用场景:
def mockFile = mock(File,file.getName()
}
你可以设置一个期望,用于匹配当一个构造函数调用时发生异常的情况:
def mockFile = mock(File,constructor("/a/path/file.txt").raises(RuntimeException))
play {
shouldFail(RuntimeException) {
new File("/a/path/file.txt")
}
}
2.9 时间匹配
Gmock也让你可以识别出一个期望到底被调用了几次,示例所示:
mockLoader.load(2).returns(3).atLeastOnce()
play {
assertEquals 3,mockLoader.load(2)
assertEquals 3,mockLoader.load(2)
}
² never() 期望不被执行
² once() 期望执行一次 (默认设置)
² atLeastOnce() 至少一次
² atMostOnce() 最多一次
² stub() 期望在任何时候都允许被调用
² times(3) 期望被调用3次
² times(2..4) 期望需要被调用2至4次
² atLeast(4) 期望至少被调用4次
² atMost(4) 期望最多被调用4次
2.10 使用匹配方法
你可以根据匹配语法在自己的期望中定制一个匹配方法。在闭包中通过断言进行匹配,如果成功,则返回true。
示例如下:
mockLoader.put("test",match { it > 5 }).returns("correct")
play {
assertEquals "correct",mockLoader.put("test",10)
}
Gmock完全兼容Hamcrest框架(Hamcrest是一个书写匹配器对象时允许直接定义匹配规则的框架)。使用的时候必须在你的类路径下添加Hamcrest类库。
示例如下:
mockLoader.put("test",is(not(lessthan(5)))).returns("correct")
play {
assertEquals "correct",10)
}
2.11 匹配次序
当方法调用需要排序的时候你可以使用排列闭包。调用的顺序期望可以申请多种mock方式。
假设要在数据库中缓存cat实例如下:
def database = mock()
def cache = mock()
ordered {
database.open()
cache.get("select * from cat").returns(null)
database.query("select * from cat").returns(["cat1","cat2"])
cache.put("select * from cat",["cat1","cat2"])
database.close()
}
如果在一些情况下你不想指定一个排序闭包,这时候你可以像如下所示代码来嵌套使用无序的闭包:
def mockLock = mock()
ordered {
mockLock.lock()
unordered {
// ...
}
mockLock.unlock()
}
2.12正则表达式匹配方法
当你设置期望的时候,可以以一个正则表达式的方式来定义这个方法。这样在匹配时就可以挑选出任何与此正则表达式相匹配的方法。示例如下:
def mock = mock()
mock./set.*/(1).returns(2)
play {
assertEquals 2,mock.setSomething(1)
}
2.13一些语法使用的捷径
Gmock 提供了很多在不同场景中有用的语法捷径。具体有以下几点:
2.13.1 Mock闭包中的期望
Mock期望可以在闭包中创建mock时设定,例如:
def mock = mock(Loader){
load(1).returns("one")
}
def mock = mock(Loader)
mock.load(1).returns("one")
2.13.2 With闭包的使用
类似于mock闭包,你可以在mock实例中使用with闭包的语法。例如:
def mock = mock(Loader)
with(mock){
load(1).returns("one")
}
2.13.3 静态闭包
静态期望可以用静态闭包来创建,例如:
def mockMath = mock(Math)
mockMath.static {
random().returns(.3)
random().returns(.6)
}
2.14 一些特殊用法
2.14.1 不继承GmockTestCase
如果在测试中你不想或不能继承GmockTestCase,那么,自Groovy 1.6之后你可以使用注解——@WithGMock。简单地注解类代码的示例如下:
import org.gmock.WithGMock
...
@WithGMock
class YourTest extends GroovyTestCase {
// ... write your test here as normal
}
另外,我们还提供了一种原始的定义一个GmockController的方式。
在测试之初,创建一个GmockController。随后,你可以使用它的mock()方法和play闭包设计单元测试用例了。
void testController(){
def gmc = new GMockController()
def mockLoader = gmc.mock()
mockLoader.load('key').returns('value')
gmc.play {
assertEquals "value",mockLoader.load('key')
}
}
2.14.2 关于equals,hashCode,toString的一点提示
Gmock为equals,ashcode和toString方法提供了默认的支持,所以,在写单元测试用例的时候就不必专门再为他们创建期望了。
如果需要,你可以在那些禁用了默认支持的方法之上创建期望,例如你可以在mock(name("Now"))的mock方法中为toString方法自定义一个名称。
三、 总结
Groovy是运行在jvm上的面向对象的动态脚本语言。最大的特点就是弱类型而且能够和java进行相互调用。这使得用它作为TDD中单元测试的编写十分具有优势。另外由于Groovy语法的简单和灵活性,还能够帮助简化单元测试的编写。
GMock是groovy环境下的mock解决方法。使用它可以很轻松的完成groovy的单元测试工作。它能够很好的模拟对象,辅助junit完成单元测试。关于GMock的使用,主要有两种方式:一是直接继承自GMockTestCase,二是在类上使用@WithGMock注解。
总结一下使用Gmock主要分为以下四个步骤:
1. 测试类需要继承自GMockTestCase,或者是使用Annotation的方式——在类声明处使用@WithGMock。
2. 创建GMockController对象,调用其mock()方法,创建一个需要模拟的对象。
4. 在GMockController对象上调用play闭包,完成需要的断言。
有一点担忧的地方:目前用于支持Groovy语言的Eclipse插件功能不是特别强大,只有一些基本的语法高亮等,没有输入提示等较高级的支持,这为这种方式的施行添加了些许困难;但相比其弱类型的特性在TDD中作为单元测试天然的优势,我个人认为如果要用TDD的方式做开发,那么这种单元测试还是值得一试的。
四、 参考资料
在整理这个学习笔记的时候参考了一些网络资源,列出来,一方面表示对作者劳动成果的尊重,另一方面也便于大家更全面的去了解上面所述的内容。
1、 Groovy官方网站上的文档:http://groovy.codehaus.org/Documentation 本文不少内容来源于此。
2、 GMock官方使用指南:http://code.google.com/p/gmock/wiki/Documentation_0_8 本文部分内容和示例翻译于此处。