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

时间:2009-07-17 14:18:07

标签: java unit-testing refactoring mocking

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

2 个答案:

答案 0 :(得分:1)

您可以单独创建一个接口及其实现,而不是使用匿名类。然后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);
    }
  }

答案 1 :(得分:0)

你可以得到幻想并“捕获”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());
}

我是否喜欢这个是冲突的 - 它暴露了你正在测试的方法的很多内部结构。但至少它是一种选择。