需要重构才能提高可测试性

问题描述:

我正在用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是模拟的一部分,我也对测试代码感兴趣...我应该对该特定命令做一个单独的测试吗?

+0

请给出一个越来越难做的测试的例子。 – 2009-07-17 14:24:35

+0

我不太了解Java,但我会说ResultSetCommand不相关。您正在测试getPersonById,以确保在给定有效的UserId时返回正确的Person,在给定无效的时候抛出异常等。如果它正常工作,则不关心它使用特定的ResultSetCommand。 – 2009-07-17 14:35:39

而不是使用匿名类,你可以分别创建一个接口和它的实现。然后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暴露了很多正在测试的方法的内部结构。但至少这是一个选择。