2008-03-19

测试的粘结度

最近一直在写操作符处理的单元测试。正如liangfei所说,想要更好的优化表达式,首先得十分了解操作符的功能,而写单元测试就是非常好的一个途径。十分赞同这个观点,所以我最近一直在写测试,也确确实实地了解了操作符的功能。

 

在测试类中如何获得操作符对象呢?我参考了一下写完的测试类,发现是 new 出来的。可是在程序中,操作符不是new出来的,而是通过一个IOC容器获得的,而且获得是某一种操作符的handler,例如下面这样:

 

Configuration config = PropertiesConfigurationLoader.loadStandardConfiguration();
// 默认会取得 StandardOperatorHandlerProvider
operatorHandlerProvider = config.getOperatorHandlerProvider();
handler = operatorHandlerProvider.getUnaryOperatorHandler(".");

 

这个handler就是处理"."符号的了,通过这个handler,再去调用"."号操作符。这么做的目的我想是为了实现操作符的重载。

 

所以,在CommonTemplate里,是不可能直接得到操作符对象的。因此,在测试类中,我也就使用了这种方式,来获得操作符进行测试。

 

不过liangfei并不同意这种方式。他认为测试操作符的时候,应该在测试类中 new 出操作符,然后对操作符进行测试。理由是这样可以把要测试的目标功能分隔开来。我的理解就是降低测试的粘结度。但是这样就产生另外一个问题,如何保证 OperatorHandler的正确性?

 

或许应该再编写测试类,从 OperatorHandler 开始,测试到每一个操作符。这样确实很麻烦。但是如果不做这种测试的话,又不能保证 OperatorHandler 的正确性。

 

那么测试的粘合度如何控制比较好?例如一个项目,有业务层,和持久层。那么当对这两个层写测试的时候,改如何进行?

 

按照所理解的TDD的方式,在实现持久层的时候,必然会写持久层的测试。在写业务层的时候,再去写业务层的测试,这时,业务层的测试也间接包含了持久层的内容。那我们是不是可以跳过持久层的测试?当然不可以。

 

所以问题又回来了,对于CommonTemplate来说,我们是不是可以跳过操作符的测试?当然不可以--事实上我们也确实没有跳过,现在写的就是操作符的测试。那么我们可以不可以跳过 OperatorHandler 的测试?也是不可以。不过,OperatorHandler 中逻辑很少,只是按照责任链的模式把变量分配到正确的操作符上。

 

因此,我们是不是可以把 OperatorHandler 的测试和操作符的测试一起做了?虽然这样做测试粘合度比较大,但是 OperatorHandler 的逻辑几乎没有,如果再单独对它进行测试,就和测试操作符完全重复了。

 

所以,是不是也可以这样考虑,两个类,虽然有关联,但是其中一个类没有逻辑,或者逻辑非常少,那么可以不考虑粘合度,而直接对这两个类一起进行测试?

 

这个问题我不知道答案。不过我还是遵从liangfei的意见,按照降低粘合度的方式修改了测试类。希望能在以后的积累中,得到一个满意的答案。

 

评论
javatar 2008-04-04
这样做是为了遵守单元测试原则:
(1) 隔离:每次只测试一个类,并Mock其所依赖的类,这样才能准确定位问题所在,并且保证没试用例的职责分明。
(2) 无序:每一个测试用例都不应该依赖于其它测试用例。
(3) 自动:验证过程可自动完成,不需要人为查看输出等。
(4) 可重复:测试用例应可重复执行,如果修改了状态,应在tearDown中恢复。

如果实在需要测试多操作符集成, 也可以自行组装, 而不依赖配置容器:
BinaryOperatorHandlerChain chain;

setUp:
List handlers = new ArrayList();
handlers.add(new ObjectFunctionOperatorHandler());
handlers.add(new ObjectPropertyOperatorHandler());
handlers.add(new ScopesGetterOperatorHandler());
handlers.add(new MapGetterOperatorHandler());

chain = new BinaryOperatorHandlerChain();
chain.setRightOperandNamed(true); // 点号的右参不作为变量
chain.setRightOperandFunctioned(true);
chain.setBinaryOperatorHandlers(handlers);

// 然后测试chain 



另外, 你说的项目中的测试也应隔离, 下面给出的是简单的测试例子:
package com.xxx.action;

import junit.framework.TestCase;

import com.xxx.action.mock.EntryBizMock;

/**
 * 入口模块Action测试
 *
 * @author 梁飞
 *
 */
public class EntryActionTestCase extends TestCase {

	// 待测试Action类 (Struts2)
	private EntryAction entryAction;

	// Biz的仿造类
	private EntryBizMock entryBizMock;

	@Override
	protected void setUp() throws Exception {
		entryBizMock = new EntryBizMock();
		entryAction = new EntryAction();
		entryAction.setEntryBiz(entryBizMock);
	}

	@Override
	protected void tearDown() throws Exception {
		entryBizMock = null;
		entryAction = null;
	}

	/**
	 * 测试登录
	 *
	 * @throws Exception
	 */
	public void testLogin() throws Exception {
		// 注入测试参数
		entryAction.setUsername("liangfei");
		entryAction.setPassword("123456");
		// 执行被测试方法
		String result = entryAction.login();
		// 断言返回值是否正确
		super.assertEquals(EntryAction.SUCCESS, result);
		// 断言Biz是否被调用
		super.assertTrue(entryBizMock.isLogin());
		// 断言Biz是否收到正确数据
		super.assertEquals("liangfei", entryBizMock.getUsername());
		super.assertEquals("123456", entryBizMock.getPassword());
	}

	/**
	 * 测试退出登录
	 *
	 * @throws Exception
	 */
	public void testLogout() throws Exception {
		// 执行被测试方法
		String result = entryAction.logout();
		// 断言返回值是否正确
		super.assertEquals(EntryAction.SUCCESS, result);
		// 断言Biz是否被调用
		super.assertTrue(entryBizMock.isLogout());
	}

}


package com.xxx.action.mock;

import com.xxx.biz.IEntryBiz;
import com.xxx.exception.LoginException;

/**
 * 入口业务的仿造实现类
 *
 * @author 梁飞
 *
 */
public class EntryBizMock implements IEntryBiz {

	// ---- login mock ----

	@Override
	public void login(String username, String password) throws LoginException {
		this.login = true;
		this.username = username;
		this.password = password;
	}

	private String username;

	/**
	 * login()所接收到的用户名
	 *
	 * @return 用户名
	 */
	public String getUsername() {
		return username;
	}

	private String password;

	/**
	 * login()所接收到的密码
	 *
	 * @return 密码
	 */
	public String getPassword() {
		return password;
	}

	private boolean login = false;

	/**
	 * login()是否被调用标识
	 *
	 * @return 调用标识
	 */
	public boolean isLogin() {
		return login;
	}

	// ---- logout mock ----

	@Override
	public void logout() {
		this.logout = true;
	}

	private boolean logout = false;

	/**
	 * logout()是否被调用标识
	 *
	 * @return 调用标识
	 */
	public boolean isLogout() {
		return logout;
	}

}


当然, 如果觉得Mock很烦, 可以用EasyMock等其它工具简化.
发表评论

您还没有登录,请登录后发表评论

yananay
搜索本博客
我的相册
D35bafa8-5bf0-475f-aa06-16f3080557a1-thumb
tdd
共 3 张
存档
最新评论