我摸、我摸、我摸摸摸

 

      如果把UT比作一个长满仙人掌,那么类依赖、外部环境就可以看做仙人掌上的刺了,为了让coder们在摸这个仙人掌时,不会被这些烦人的刺给扎到手,现在在Java行业里,市面上出现了许多的mock服务,这里简称“摸客”,各显其能,就是为了将这些刺给剃光,让coder们摸起来顺手舒适。真正的单元测试运行起来通常都是非常迅速的,因为它不需要依赖于数据库、服务器等等运行设施,这样才能大大提高coder的生产力,而结合这些摸客和JUnit,现在UT就能够真真正正达到这个目标了。Easymock,就是其中的一个摸客,正如其名,这个“一级”摸客(谐音,easy让我想到了中文的一级,Easymock的作者估计挺熟悉中文,给起了个这么个好名字),在对接口依赖、外部环境打桩方面非常easy,可以大大提高coder编写UT代码时的效率。

      Easymock使用起来很简单,用Easymock写的UT代码,通常由录制、回放(执行)、验收这三个部分来构成:
1、录制:构造待测试方法中依赖的接口对象(后续统称为mock对象),然后指定mock对象在待测试代码中将有哪些方法会被调用,以及调用的结果;
2、回放(replay):执行待测试方法;
3、验收(verify):检查各个mock对象(摸客对象)被录制的方法最终是否被正确调用了,以及待测试代码根据这些mock对象的执行结果是否做了正确的事。
一般我们都是先code代码,然后写UT代码进行测试。如果使用Easymock,整个code、测试的过程就是:code、录制、回放、验收,这好比拍电影,先写剧本(待测试方法),接着按照剧本准备一切(包括道具、演员等),然后开拍,最好导演进行验收,如果一切是按着剧本演的,那么就可以GO,否则得NG重来。

      下面是某女按照其爱情观编写出的一个剧本(类),描述了男孩和女孩之间的爱情故事,显然,这个故事需要一男一女两个角色(依赖Boy和Girl接口,这里Boy和Girl都是接口类,Boy可以是有钱的Boy,也可以是个穷光蛋,Girl可以是个美女,也可以是只暴龙)。现在我们试着用“一级摸客”的拍摄流程,来导演这个剧本。

 

class Love
{
	private Boy boy;

	private Girl girl;

	public Love(Boy boy, Girl girl) {
		this.boy = boy;
		this.girl = girl;
	}

	public Object love() 
	{
		if(boy.有房() && boy.有车()) {
			return girl.嫁给(boy); 
		} else if (girl.愿意等()) {
			while(boy.活着() && girl.活着()) {
				for(int day=1; day <= 365; day++) {
					if (day == 节日.情人节) {
						if (boy.givegirl("玫瑰")) {
							girl.感情递增(); 
						} else {
							girl.感情递减(); 
						}
					}
					if(day == girl.生日()) {
						if(boy.givegirl("玫瑰")) {
							girl.感情递增(); 
						} else {
							girl.感情递减(); 
						}
					}
					boy.拼命赚钱();
				}
				boy.年龄递增();
				girl.年龄递增();
				if(boy.有房() && boy.有车()) {
					return girl.嫁给(boy); 
				} else if(!(boy.年收入() > 100000 && girl.感情() > 0)) {
					return girl.离开(boy);
				}
			}
			return girl.离开(boy);
		} else {
			return girl.离开(boy);
		}
	}
}

 首先,在我们的TestCase中引入“一级摸客”的包:

import static org.easymock.EasyMock.*;

接着分别为我们的拍摄需要的演员Boy和Girl创建一个摸客对象(mock对象):

Boy mockBoy = createMock(Boy.class);
Girl mockGirl = createMock(Girl.class);

createMock是EasyMock用于创建摸客对象的方法,入参是你想要创建的桩的类class。现在我们开始开拍,首先让我们的演员——男女摸客不做任何事情,直接回放:

replay(mockBoy);
replay(mockGirl);
love.love();

在测试我们待测试方法之前,所有将要用到的摸客对象都必须通过replay方法切换到回放状态,否则调用摸客对象上的方法,EasyMock是毫不留情的抱怨的。
完整的代码大概如下:

public void testLove() {
	Boy mockBoy = createMock(Boy.class);
	Girl mockGirl = createMock(Girl.class);
	Love love = new Love(mockBoy, mockGirl);
	replay(mockBoy);
	replay(mockGirl);
	love.love();
}

第一个场景的所有事项都已经搞定了,让我们在跑跑我们的testcase,结果导演大喊一声“咔”:

java.lang.AssertionError: 
  Unexpected method call 有房():
	at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:32)
	at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:61)
	at $Proxy4.有房(Unknown Source)

因为这个love脚本中,它是期望我们的男主角要回应【有房】和【有车】这两个方法调用的,结果我们的男摸客却非常木呐,什么都没有做,所以肯定会被我们的大导演NG掉的。
      现在假设我们的男主角是个有车、有房的金龟婿,看看我们的女主角是否会嫁给她,按照我们的脚本是会的:

public void testLove() {
	Boy mockBoy = createMock(Boy.class);
	Girl mockGirl = createMock(Girl.class);
	Love love = new Love(mockBoy, mockGirl);
	expect(mockBoy.有房()).andReturn(true);
	expect(mockBoy.有车()).andReturn(true);
	expect(mockGirl.嫁给(mockBoy)).andReturn("结婚");
	replay(mockBoy);
	replay(mockGirl);
	assertEquals("结婚", love.love());
	verify(mockBoy);
	verify(mockGirl);
}

      这时我们的导演满意的笑了——junit变绿了。让我们来看看这个场景中,我们都做了些什么才使得我们的大导演满意了。首先,我们分别调用了男摸客对象的有房、有车接口,假如你认为待测试方法中会调用mock对象的某个方法,那么在replay之前,你需要调用下mock对象的相应方法(就像上面的mockBoy.有房()一样),然后待测试方法在执行过程中如果没有执行你指定的方法,那么测试就通不过。一般会得到类似下面的信息(这里假设把love方法中的boy.有房()语句删除掉):

java.lang.AssertionError: 
  Expectation failure on verify:
    有房(): expected: 1, actual: 0
	at org.easymock.internal.MocksControl.verify(MocksControl.java:101)

假如你的方法是有返回值的,那么你就需要用expect和andReturn来指定摸客方法的返回值,如上面的:

expect(mockBoy.有房()).andReturn(true);

如果你希望你的摸客方法抛出某个异常,则可以用andThrow来代替andReturn,方法参数是你希望抛出的异常类对象:

expect(mockBoy.有房()).andThrow(new RuntimeException());

最后,在测试方法执行过后,我们除了用assertTrue、assertEquals等方法来验证方法的执行结果外,别忘了要verify我们的摸客对象,否则某些我们指定的摸客对象的方法调用在被测试方法中如果没有被执行的话,这些就会成为漏网之鱼了,就上面举的例子一样,如果没有verify(mockBoy)的话,那么把love方法中的body.有房()删掉后,测试代码依然能够跑通,但这并不是我们想要的。
      OK,大部分女孩都会愿意嫁给个有钱的主,如果男主角是个没房、没车的主呢?那就要看女主角愿不愿意等了,假如她认为他是个“潜力股”,她愿意等他发达,那来看看故事的发展过程:

public void testGirlWantToWait() {
	Boy mockBoy = createMock(Boy.class);
	Girl mockGirl = createMock(Girl.class);
	Love love = new Love(mockBoy, mockGirl);
	expect(mockBoy.有车()).andReturn(false);
	expect(mockGirl.愿意等()).andReturn(true);
	expect(mockBoy.活着()).andReturn(true).anyTimes();
	expect(mockGirl.活着()).andReturn(true).anyTimes();
	expect(mockGirl.生日()).andReturn(100).anyTimes();
	expect(mockBoy.givegirl("玫瑰")).andReturn(true).times(4);
	mockGirl.感情递增();
	expectLastCall().times(4);
	mockBoy.拼命赚钱();
	expectLastCall().anyTimes();
	mockBoy.年龄递增();
	expectLastCall().times(2);
	mockGirl.年龄递增();
	expectLastCall().times(2);
	expect(mockBoy.有房()).andReturn(false).times(2);
	expect(mockBoy.年收入()).andReturn(100001);
	expect(mockGirl.感情()).andReturn(1);
	expect(mockBoy.年收入()).andReturn(100000);
	expect(mockGirl.离开(mockBoy)).andReturn("分手");
	replay(mockBoy);
	replay(mockGirl);
	assertEquals("分手", love.love());
	verify(mockBoy);
	verify(mockGirl);
}

这里,我们用了一个新的方法expectLastCall()来表示上一个mock对象的方法调用,另外还用了两个新的方法:anyTime()和times(***),用于表示上一个mock对象的方法调用将会被执行的次数,anyTime()表示执行任意次,times(***)表示执行指定次数。上面我们假定了男主角没有车且女主角愿意等,然后两人继续发展了两年:

mockBoy.年龄递增();
expectLastCall().times(2);

在这两年内男主角为了讨女主角欢心,每逢重大节日都送女主角花,但是仅仅第二年的年收入比女主角预期的少了一块,就被女主角“飞掉”了(真够现实的^_^):

expect(mockBoy.年收入()).andReturn(100000);
expect(mockGirl.离开(mockBoy)).andReturn("分手");

现在跑跑我们的测试,看看这种预想按我们的love剧本演下去,其结果是不是和我们预想的一样,结果我们的junit又再次亮起了绿灯,证明我们的剧本写的没有问题。
      EasyMock在判断某个预期的方法调用最终是否被执行了时,对于方法上的参数对象的匹配缺省是通过对象的equals方法来比较预期方法调用是指定的参数与实际方法调用时指定的参数是否匹配的。这种缺省方式可能不能满足我们的需求,例如有一个接口,需要一个字符串数组做为参数:

String[] params = new String[]{“param1”, “param2”};
expect(mock.parse(params)).andReturn(2);

这里我们期望的是,接口在实际被调用时传入的参数是一个长度为2,第一个元素为param1,第二个元素为param2就认为是正确的,但由于EasyMock是用equals进行参数匹配的,而数组的equals方法是按照对象地址进行匹配的,这样即使数组内容相同,但由于两个数组对象本身不是同一个对象,这时EasyMock就开始报错,这不是我们想要的。为了解决这个问题,EasyMock提供了其它一些进行参数匹配的方法,如aryEq:

String[] params = new String[]{“param1”, “param2”};
expect(mock.parse(aryEq(params))).andReturn(2);

aryEq按照数组元素进行匹配。。其它常用的参数匹配器如下:

eq(X value):当实际值和预期值相同时匹配。适用于所有的简单类型和对象;
anyBoolean()、anyByte()、anyChar()、anyDouble()、anyFloat()、anyInt()、anyLong()、anyObject():匹配任意值,假如你只想测试下你的mock对象是否被调用,而不管调用时使用的参数,就可以用这些方法;
aryEq(X value):两个数组的内容相同时匹配(当Arrays.equals()返回true时匹配),适用于任意基本类型与对象的数组;
isA(Class clazz):当实际值为指定class的对象时匹配(也可以是指定class子类的对象),适用于对象;
isNull():当实际值为null时匹配,适用于对象;
notNull():当实际值不为nll时匹配,适用于对象;
same(X value):当实际值与预期值为同一个对象时匹配,即=返回true时匹配,适用于对象;
此外还有适用于数值比较的lt、gt、leq、geq;适用于字符串比较的startsWith、contains、endsWith、matches、find;表示逻辑关系的:and、or、not等。
虽然EasyMock已经内建了那么多匹配器,但我们有时会遇到需要自定义参数匹配器的情况。通过下面两个步骤来完成自定义参数匹配器的实现:
1.    定义新的参数匹配器,实现org.easymock.IArgumentMatcher接口;
2.    声明静态方法用于向EasyMock报告自定义参数匹配器;
假设我们需要一个能够对List内容进行匹配的匹配器(只是举例,其实根本不需要用到这种匹配器),首先我们来实现IArgumentMatcher:

import java.util.List;
import org.easymock.IArgumentMatcher;

public class ListArgumentMatcher implements IArgumentMatcher {
	private List expected;

	public ListArgumentMatcher(List expected) {
		this.expected = expected;
	}

	public void appendTo(StringBuffer buffer) {
		buffer.append("eqCollection(");
		buffer.append(expected.getClass().getName());
		buffer.append(" with values:");
		buffer.append(expected);
		buffer.append(")");
	}

	public boolean matches(Object actual) {
		if (actual instanceof List) {
			List actualList = (List) actual;
			if (expected.size() == actualList.size()) {
				for (int i = 0; i < expected.size(); i++) {
					if ((expected.get(i) == null && actualList.get(i) != null)
							|| !expected.get(i).equals(actualList.get(i))) {
						return false;
					}
				}
				return true;
			}
			return false;
		}
		return false;
	}
}

 如上所示,IArgumentMatcher接口需要实现两个方法:appendTo用于添加表示该匹配器的字符串到指定的StringBuffer中;matches用于实现对象的匹配。
    然后在我们的TestCase中添加eqList的静态方法用来报告这个匹配器:

public static <T extends List> T eqList(T in) {
	reportMatcher(new ListArgumentMatcher(in));
	return null;
}

 这里我们调用了EasyMock的reportMatcher方法报告了我们的参数匹配器,然后返回一个值,一般返回null、false或0就可以了。
      在上面的例子中,所有的Mock对象都是针对于接口来创建的,这是EasyMock的基本能力,对于新开发的系统来说,这应该足够了,因为我们可以约束新代码需要基于接口进行编程,但现实中也存在既有代码没有按接口编程来实现,这时怎么办呢?感谢EasyMock,它提供了一个扩展包,用于基于类创建mock对象,针对类创建mock对象的方式与针对接口创建mock对象的方式完全一样,惟一不同是这时需要引入:

import static org.easymock.classextension.EasyMock.*;

包。例如AccountDao是一个普通类,没有实现任何接口,那么创建AccountDao的mock对象和上面创建Boy接口的mock对象完全一样:

AccountDao mockAccountDao = createMock(AccountDao.class);

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(编程,单元测试,JUnit,脚本,笑话)