使用简单的方法填充ResultSet数据

24

我希望能够模拟一个ResultSet。认真的。 我正在重构一个大而复杂的代码块,它从ResultSet解析数据,并且我希望我的代码行为完全一致。因此,我需要编写单元测试以测试被重构的部分,以便测试这个。

在Google搜索之后,我想到了两个想法:

  1. 使用EasyMock,编写非常长的mocking序列。非常糟糕的解决方案:很难添加初始数据,很难更改数据,需要进行大量的测试调试。
  2. 使用Apache Derby或HSQLDB创建内存中的数据库,从文件或字符串数组填充它,使用一些神奇的InMemoryDBUtils.query(sql)查询。然后使用那个ResultSet。不幸的是,我没有找到任何神奇的InMemoryDBUtils可以快速编写测试 :-)。IBM文章“使用Derby进行持久性的隔离单元测试”似乎正是我需要的...

第二种方法看起来更容易一些,而且更易于支持。

您有什么建议可以创建这样的模拟吗?(当然要除了医生)?我是否遗漏了某个神器?可能,DBUnit是这个工具吗?

6个回答

40

我曾经使用MockRunner中的MockResultSet类获得了成功。它允许您创建实现ResultSet接口的类,并为每个列和行设置值。

如果您的方法使用的结果集大小合理,那么您应该能够轻松地创建返回所需值的测试。

这里是一个简单的示例:

MockResultSet rs = new MockResultSet("myMock");

rs.addColumn("columnA", new Integer[]{1});
rs.addColumn("columnB", new String[]{"Column B Value"});
rs.addColumn("columnC", new Double[]{2});

// make sure to move the cursor to the first row
try
{
  rs.next();
}
catch (SQLException sqle)
{
  fail("unable to move resultSet");
}

// process the result set
MyObject obj = processor.processResultSet(rs);

// run your tests using the ResultSet like you normally would
assertEquals(1, obj.getColumnAValue());
assertEquals("Column B Value", obj.getColumnBValue());
assertEquals(2.0d, obj.getColumnCValue());

你好mjd79,感谢你的回答。在我的情况下,问题是每个测试用例肯定会有大约100条记录,大约有10个测试用例 :-) 每个记录都有10个字段,包括日期、数字和字符串。因此,我更喜欢将模拟数据存储在文件中,最好是以INSERT INTO形式,可以直接从几乎所有DB客户端中获取。MockRunner看起来很不错,但我会将其保留给其他情况。再次感谢您的时间。 - DiaWorD
mockrunner 是一个很好的资源,用于模拟批处理作业测试的简单结果集。非常感谢! - EdgeCaseBerg

11
据我所知,DBUnit并不提供结果集,但它可以帮助您填充内存数据库。
我认为使用mocking框架是错误的方法。Mocking是关于测试行为和交互,而不仅仅是返回数据,因此它可能会妨碍您。
相反,我建议实现一个结果集接口,或者创建一个结果集接口的动态代理,指向一个只实现您关心的方法而无需实现整个结果集的类。您可能会发现维护一个类就像维护一个内存数据库一样容易(前提是测试数据集是一致的),并且可能更容易调试。
如果数据稍微复杂,您可以用DBUnit来支持这个类,在测试期间使用dbunit获取结果集的快照,然后让dbunit从xml中读取数据,并让您的虚拟结果集从dbunit的类中读取数据。这是一个合理的方法。
如果这些类之间的耦合性很强,需要读取在同一测试中修改的数据,则应使用内存数据库。即使如此,我也会考虑使用真实数据库的副本,直到您成功解除该依赖项。
一个简单的代理生成方法:
private static class SimpleInvocationHandler implements InvocationHandler {
    private Object invokee;

    public SimpleInvocationHandler(Object invokee) {
        this.invokee = invokee;
    }

    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        method = invokee.getClass().getMethod(method.getName(), method.getParameterTypes());
        if (!method.isAccessible()) {
            method.setAccessible(true);
        }
        try {
            return method.invoke(invokee, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
    }
}

public static <T> T generateProxy(Object realObject, Class... interfaces) {
    return (T) Proxy.newProxyInstance(realObject.getClass().getClassLoader(), interfaces, new SimpleInvocationHandler(realObject));
}

你好Yishai,感谢您的反馈。我将为10个或更多测试模拟大约100个记录,每个记录包含10个字段。据我所知,这将使我使用DBUnit将数据存储在文件中,并使用自定义的ResultSet实现。再次感谢你的回答。 - DiaWorD
不客气。如果这10个测试需要不同的数据,那么这是一个合理的方法。使用DBUnit,您可以将ResultSet写入XML,然后在测试中引用它。 - Yishai

7

Mockrunner 可以自动加载 CSV 或 XML 文件并创建 MockResultSet。它还可以模拟 Connection 和 Statement,因此所有的 JDBC 都可以正常工作,而无需将 JDBC 驱动程序添加到类路径中。


6

我为这个相同的情况写了一些东西。你可以使用Mockito模拟结果集。你也可以通过模拟resultset.next()来循环遍历模拟行。

// two dimensional array mocking the rows of database.
String[][] result = { { "column1", "column2" }, { "column1", "column2" } };

@InjectMocks
@Spy
private TestableClass testableClass;

@Mock
private Connection connection;

@Mock
private Statement statement;

@Mock
private ResultSet resultSet;

@BeforeTest
public void beforeTest() {
    MockitoAnnotations.initMocks(this);
}

@BeforeMethod
public void beforeMethod() throws SQLException {
    doAnswer(new Answer<Connection>() {
        public Connection answer(InvocationOnMock invocation)
                throws Throwable {
            return connection;

        }
    }).when(testableClass).getConnection();

    when(connection.createStatement()).thenReturn(statement);
    when(statement.executeQuery(anyString())).thenReturn(resultSet);
    final AtomicInteger idx = new AtomicInteger(0);
    final MockRow row = new MockRow();

    doAnswer(new Answer<Boolean>() {

        @Override
        public Boolean answer(InvocationOnMock invocation) throws Throwable {
            int index = idx.getAndIncrement();
            if (result.length > index) {
                String[] current = result[index];
                row.setCurrentRowData(current);
                return true;
            } else
                return false;

        }

        ;
    }).when(resultSet).next();

    doAnswer(new Answer<String>() {

        @Override
        public String answer(InvocationOnMock invocation) throws Throwable {
            Object[] args = invocation.getArguments();
            int idx = ((Integer) args[0]).intValue();
            return row.getColumn(idx);
        }

        ;
    }).when(resultSet).getString(anyInt());
}

static class MockRow {
    String[] rowData;

    public void setCurrentRowData(String[] rowData) {
        this.rowData = rowData;
    }

    public String getColumn(int idx) {
        return rowData[idx - 1];
    }
}

2

如果适用的话,您现在可以从真实数据源获取结果集,对其进行序列化并保存文件。然后,您可以为每个单元测试反序列化该结果集,这样您就可以顺利进行了。


你好GWLlosa,也感谢你的反馈。我认为DBUnit将帮助我将数据存储在文件中。 - DiaWorD

1
只要你不调用大多数ResultSet方法,我可能会将分隔文本文件加载到二维数组中,并实现我实际需要的方法,让其余方法抛出UnsupportedOperationException(这是我的IDE中未完成方法的默认实现)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接