需要重构才能提高可测试性
问题描述:
我正在用mockito测试一个简单的DAO层,但是我发现一个问题,基本上是一个难以测试的界面,我想知道您是否可以给我一些洞见...需要重构才能提高可测试性
这是我要测试的方法:
public Person getById(UserId id) {
final Person person = new PersonImpl();
gateway.executeQuery(GET_SQL + id.getUserId(), new ResultSetCommand(){
public int work(ResultSet rs) throws SQLException {
if(rs.next()){
person.getName().setGivenName(rs.getString("name"));
person.getName().setFamilyName(rs.getString("last_name"));
}
return 0;
}
});
return person;
}
我使用DatabaseGateway这是我的Java代码和SQL之间的接口,并且该方法接受一个匿名类,这是网关的方法的executeQuery:
public int executeQuery(String sql, ResultSetCommand cmd) {
try{
Connection cn = createConnection();
PreparedStatement st = cn.prepareStatement(sql);
int result = cmd.work(st.executeQuery());
cn.close();
return result;
}catch(Exception e){
throw new RuntimeException("Cannot Create Statement for sql " + sql,e);
}
}
问题在于,由于那个匿名类,测试PersonDAO变得越来越困难。
我可以重构整个代码,甚至可以删除匿名类,如果有人提出更好的设计(我敢肯定有更简单的一个,但我似乎无法找到它)。
谢谢大家的建议。
PD:如果您需要了解更多信息,请随时问
编辑:测试这是很难做到的
public void testGetPersonById(){
DatabaseGateway gateway = mock(DatabaseGateway.class);
when(gateway.executeQuery(anyString(),any(ResultSetCommand.class)));
PersonDAO person_dao = new PersonDAOImpl(gateway);
Person p = person_dao.getById(new UserId(Type.viewer,"100"));
}
看到了吗? ResultCommand是模拟的一部分,我也对测试代码感兴趣...我应该对该特定命令做一个单独的测试吗?
答
而不是使用匿名类,你可以分别创建一个接口和它的实现。然后executeQuery方法将会有一个String和这个接口作为参数。
所以你的测试将保持不变。你将能够在另一个测试中对工作方法进行分离(对你的接口实现进行测试),这看起来很难测试。
结果会是这样的:
public Person getById(UserId id) {
final Person person = new PersonImpl();
gateway.executeQuery(GET_SQL + id.getUserId(), new MyInterfaceImpl(person));
return person;
}
,
public int executeQuery(String sql, MyInterface cmd) {
try{
Connection cn = createConnection();
PreparedStatement st = cn.prepareStatement(sql);
int result = cmd.work(st.executeQuery());
cn.close();
return result;
}catch(Exception e){
throw new RuntimeException("Cannot Create Statement for sql " + sql,e);
}
}
答
你可以让幻想和 “捕获” ResultSetCommand
ARG,然后模拟回调就可以用模拟ResultSet
:
/**
* Custom matcher - always returns true, but captures the
* ResultSetCommand param
*/
class CaptureArg extends ArgumentMatcher<ResultSetCommand> {
ResultSetCommand resultSetCommand;
public boolean matches(Object resultSetCommand) {
resultSetCommand = resultSetCommand;
return true;
}
}
public void testGetPersonById(){
// setup expectations...
final String lastName = "Smith";
final String firstName = "John";
final CaptureArg captureArg = new CaptureArg();
DatabaseGateway gateway = mock(DatabaseGateway.class);
ResultSet mockResultSet = mock(ResultSet.class);
when(gateway.executeQuery(anyString(), argThat(captureArg)));
when(mockResultSet.next()).thenReturn(Boolean.True);
when(mockResultSet.getString("name")).thenReturn(firstName);
when(mockResultSet.getString("last_name")).thenReturn(lastName);
// run the test...
PersonDAO person_dao = new PersonDAOImpl(gateway);
Person p = person_dao.getById(new UserId(Type.viewer,"100"));
// simulate the callback...
captureArg.resultSetCommand.work(mockResultSet);
// verify
assertEquals(firstName, person.getName().getGivenName());
assertEquals(lastName, person.getName().getFamilyName());
}
我很矛盾,我是否喜欢这个 - 我t暴露了很多正在测试的方法的内部结构。但至少这是一个选择。
请给出一个越来越难做的测试的例子。 – 2009-07-17 14:24:35
我不太了解Java,但我会说ResultSetCommand不相关。您正在测试getPersonById,以确保在给定有效的UserId时返回正确的Person,在给定无效的时候抛出异常等。如果它正常工作,则不关心它使用特定的ResultSetCommand。 – 2009-07-17 14:35:39