以前我在小公司,完成项目功能是终极目标。开发人员很害怕需求变化,因为他们改怕了。那问题出在哪里呢?后来我仔细想想,是没有做测试造成。那开发人员为什么如此害怕需求变化,我举个例子,a服务给b服务和c服务调用,后来需求改变,导致a服务无法满足b服务,能完成自身的功能是天大的事,于是没有和别人沟通把a服务直接改了。项目上线,突然有一天客户打电话说你们网站这里出问题,那里出问题,以前都不会的啊。你们怎么弄的。于是根据页面错误信息,开发人员很快找到错误根源,原来a服务改动,导致b服务不正常。而d,e,f服务依赖于b,那么导致d,e,f相关功能都出错了。立马动手改,改完上线,能知道的问题都没了,哈哈,真高兴,可是不能高兴太早哇,也许还有潜在bug。
软件的bug是无法避免,但是我们可以尽量减少bug,不断提升代码质量。刚我也说过,上述问题造成的原因是没有做测试。测试包括很多了,单元测试、集成测试和功能测试等等。既然测试如此重要,每完成一个类都能进行测试。
以前也许你比较纠结,没有好的工具,现在java社区非常活跃,我们可以选择的太多太多了:junit4,jmock,mockito,easymock,TestNg等等。如果你用过grails,那么你更清楚,此类快速开发框架已经帮我们集成好了。使用起来非常简单。所以今天我主要讲述下grails的单元测试。
假设需求:我们给每个用户分配工作,每个人都要完成两件事情,第一件事情:根据自己的用户名返回欢迎信息;第二件事情:根据自己的地址返回国家地区。
详细设计
用户信息类:
package com.test.domian
class User {
int id
String name
String address
static constraints = {
}
}
工作服务接口:
package com.test.services
class WorkService {
/**
* 根据用户名返回欢迎字符
* @param userName
* @return
*/
def processWorkOne(String userName) {
}
/**
* 根据地址返回地区
* @param address
* @return
*/
def processWorkTwo(String address){
}
}
用户工作服务:
package com.test.services
import com.test.domian.User
class UserService {
def workService
def doWork() {
def userList = User.list()
userList.each {
it.name = workService.processWorkOne(it.name)
it.address = workService.processWorkTwo(it.address)
}
}
}
我们重点来看下测试类:
package com.test.services
import grails.test.*
import com.test.domian.User
class UserServiceTests extends GrailsUnitTestCase {
protected void setUp() {
super.setUp()
}
protected void tearDown() {
super.tearDown()
}
void testDoWork() {
//构造数据,类似于数据库存在三条记录
def user1 = new User(id:1, name:"lucy", address:"hangzhou")
def user2 = new User(id:2, name:"lily", address:"wenzhou")
def user3 = new User(id:3, name:"lilei", address:"beijing")
mockDomain User, [user1, user2, user3]
//mock WorkService接口的processWorkOne方法和processWorkTwo方法
def workControl = mockFor(WorkService)
def userCount = User.count()
while(userCount-- > 0){
workControl.demand.processWorkOne(1..1){String userName ->
return "hello world, " << userName
}
workControl.demand.processWorkTwo(1..1){String address ->
return "location in " << address
}
}
def workService = workControl.createMock()
//把构造好的workservice传给userservice
UserService userService = new UserService()
userService.workService = workService
userService.doWork()
def user4 = User.findById(1)
assertEquals "hello world, lucy", user4.name
assertEquals "location in hangzhou", user4.address
}
}
以下着重来具体说明:
1、
mockDomain方法就是构造数据,包括domain类的动态方法都可以使用,比如:save(),list(),findby*()等。代码中的User.count(); User.list();就是因为调用了mockDomain方法才可以正常使用。如果是集成测试的话,grails会帮我们构造好,可以直接使用。但这里是单元测试,所以需要自己mock。
2、mockFor方法就是给WorkService构造一个对象,然后给workControl对象的demand代理创建两个UserService中用的processWorkOne和processWorkTwo方法,代码中用到了1..1,表示mock对象只能调用这个方法一次,为什么要循环三次设置processWorkOne和processWorkTwo方法呢?因为我们在UserService是对三个对象分别进行调用处理这两件事情。也许你会想,干嘛不直接把1..3(最少调用一次,最多调用三次)。是的,我最开始也是这么来处理,可是单元测试就是同不过。
如果把UserService类中的
workControl.demand.processWorkOne(1..1){String userName ->
return "hello world, " << userName
}
改成
workControl.demand.processWorkOne(1..3){String userName ->
return "hello world, " << userName
}
然后把
UserServiceTests类中的:
userList.each {
it.name = workService.processWorkOne(it.name)
it.address = workService.processWorkTwo(it.address)
}
改成
userList.each {
it.name = workService.processWorkOne(it.name)
it.name = workService.processWorkOne(it.name)
it.name = workService.processWorkOne(it.name)
it.address = workService.processWorkTwo(it.address)
}
单元测试可以通过,但是改成这样
userList.each {
it.name = workService.processWorkOne(it.name)
it.name = workService.processWorkOne(it.name)
it.address = workService.processWorkTwo(it.address)
it.name = workService.processWorkOne(it.name)
}
单元测试通不过。
以上就是表明1..3的含义:这个方法要连续被调用至少一次,至多三次。
但是有的人说我在UserService中就要这么写
userList.each {
it.name = workService.processWorkOne(it.name)
it.name = workService.processWorkOne(it.name)
it.address = workService.processWorkTwo(it.address)
it.name = workService.processWorkOne(it.name)
}
那我要怎么改单元测试才能通过?
我们把UserServiceTests的demand这段代码
workControl.demand.processWorkOne(1..1){String userName ->
return "hello world, " << userName
}
workControl.demand.processWorkTwo(1..1){String address ->
return "location in " << address
}
改成
workControl.demand.processWorkOne(1..2){String userName ->
return "hello world, " << userName
}
workControl.demand.processWorkTwo(1..1){String address ->
return "location in " << address
}
workControl.demand.processWorkOne(1..1){String address ->
return "location in " << address
}
这样就通过了。
以上就是说明构造出来的函数只能按照构造的顺序调用。今天就是因为这个花了我好长时间啊,希望我理解是正确的。如有不对,请留言纠正。