如何测试数据访问层?


17

我有一个DAO方法,该方法利用Spring进行JDBC访问。它计算出卖家出售商品的成功率。

这是代码:

public BigDecimal getSellingSuccessRate(long seller_id) {
    String sql = "SELECT SUM(IF(sold_price IS NOT NULL, 1, 0))/SUM(1) 
                  FROM transaction WHERE seller_id = ?";
    Object[] args = {seller_id};
    return getJdbcTemplate().queryForObject(sql, args, BigDecimal.class);
}

我应该如何使用JUnit测试此方法或任何DAO方法?有哪些最佳实践来测试数据访问逻辑?我正在考虑针对加载了一些数据的可嵌入数据库进行测试,但是就RDBMS和架构而言,我们不应该进行类似于生产环境的集成测试吗?


DBUnit。专为解决您的问题而设计。
塞尔吉奥

Answers:


15

使用“真实的”数据库进行单元测试的问题是测试的设置,删除和隔离。您不需要启动一个全新的MySQL数据库并仅为一个单元测试创​​建表和数据。与此有关的问题与数据库的外部性质有关,并且测试数据库已关闭,单元测试失败。确保您具有用于测试的唯一数据库也存在问题。它们可以克服,但是有一个简单的答案。

模拟数据库是一种选择,但是它不会测试实际运行的查询。当您要确保来自DAO的数据正确通过系统时,它可以用作更简单的解决方案。但是,为了测试DAO本身,您需要在DAO背后提供一些数据,并且查询可以正确运行。

首先要做的是使用内存数据库。 HyperSQL是一个极好的选择,因为它具有模拟另一个数据库的方言的能力-从而使数据库之间的细微差别保持不变(数据类型,功能等)。hsqldb还具有一些不错的单元测试功能。

db.url=jdbc:hsqldb:file:src/test/resources/testData;shutdown=true;

这将从数据库加载数据库的状态(表,初始数据)。 testData文件中。 shutdown=true最后一个连接关闭时,它将自动关闭数据库。

使用依赖注入,让单元测试选择一个与生产(或测试或本地)构建使用的数据库不同的数据库。

然后,您的DAO使用注入的数据库,您可以针对该数据库启动测试。

然后,单元测试将类似于(为简洁起见,不包含大量无聊的内容):

    @Before
    public void setUpDB() {
        DBConnection connection = new DBConnection();
        try {
            conn = connection.getDBConnection();
            insert = conn.prepareStatement("INSERT INTO data (txt, ts, active) VALUES (?, ?, ?)");
        } catch (SQLException e) {
            e.printStackTrace();
            fail("Error instantiating database table: " + e.getMessage());
        }
    }

    @After
    public void tearDown() {
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private void addData(String txt, Timestamp ts, boolean active) throws Exception {
        insert.setString(1, txt);
        insert.setTimestamp(2, ts);
        insert.setBoolean(3, active);
        insert.execute();
    }

    @Test
    public void testGetData() throws Exception {
        // load data
        Calendar time = Calendar.getInstance();
        long now = time.getTimeInMillis();
        long then1h = now - (60 * 60 * 1000);  // one hour ago
        long then2m = now - (60 * 1000 * 2);   // two minutes ago
        addData("active_foo", new Timestamp(then1h), true);     // active but old
        addData("inactive_bar", new Timestamp(then1h), false);  // inactive and old
        addData("active_quz", new Timestamp(then2m), true);     // active and new
        addData("inactive_baz", new Timestamp(then2m), false);  // inactive and new

        DataAccess dao = new DataAccess();
        int count = 0;
        for (Data data : dao.getData()) {
            count++;
            assertTrue(data.getTxt().startsWith("active"));
        }

        assertEquals("got back " + count + " rows instead of 1", count, 1);
    }

因此,您有一个调用DAO的单元测试,并且正在使用在测试过程中一直存在的动态数据库中设置的数据。您不必担心外部资源或运行前数据库的状态,也不必担心会恢复到已知状态(嗯,“已知状态”是“不存在”,这很容易恢复)。

在设置数据库,创建表和加载数据时,DBUnit可以使我所描述的过程更加简单。如果由于某种原因需要使用实际数据库,那么到目前为止,这是更好的工具。

上面的代码是我为GitHub上的概念验证TestingWithHsqldb而编写的Maven项目的一部分


2
我不知道HSQL可以模拟另一个数据库供应商的方言的部分。谢谢。
迈克尔

1
@Dog可以通过数据库属性来完成,例如sql.syntax_mys=true更改hsqldb的工作方式:“此属性设置为true时,将支持TEXT和AUTO_INCREMENT类型,并且还可以与该方言的其他方面兼容。” 而sql.syntax_ora=true做“这个属性设置为true时,启用了非标准类型的支持。这也使DUAL,ROWNUM,NEXTVAL CURRVAL和语法和,也让这个方言的一些其他方面的兼容性。”

DBUnit就是这样:)
Silviu Burcea 2013年

@SilviuBurcea DBUnit当然使建立复杂的数据库测试环境的许多“麻烦”比手工完成要容易得多。有时,如果需要知道如何手工操作仍然有用(上面提到的“手工”方法可以迁移到其他不能使用DBUnit的语言)。

您可以看看Acolyte
cchantep,2016年

2

首先,永远不要在生产环境中进行测试。您应该具有一个可以反映生产环境并在其中进行集成测试的测试环境。

如果这样做,那么您可以做很多事情。

  • 编写单元测试,以测试是否使用模拟框架(例如Mockito)将适当的SQL提交给模拟项。这将确保您的方法正在执行应做的事情,并且使集成变得毫无意义。
  • 编写测试SQL脚本,以证明您在单元测试中测试的SQL的适当性。这可以帮助您解决可能遇到的任何调优问题,因为您还可以根据测试脚本运行说明等。
  • 使用@Sergio提到的DBUnit。

糟糕,当我说生产环境时,我实际上是在模拟它。感谢您的答复,我将介绍Mockito,因为这也是我一直想学习的内容。
迈克尔

1

在我们的项目中,每个开发人员都在运行一个空数据库,其结构与生产数据库相同。

在每个单元测试TestInitialize中,我们创建与数据库的连接和事务以及每个测试所需的一些默认对象。每个方法或类结束后,所有内容都会回滚。

这样,可以测试sql层。实际上,每个查询或数据库调用都必须以这种方式进行测试。

缺点是它运行缓慢,因此我们将其与常规单元测试放在一个单独的项目中。可以通过使用内存数据库来加快此过程,但想法仍然相同。


如果使用内存数据库,则可以使用运行所有测试套件之前的删除创建方法来代替回滚事务,这会快很多。
Downhillski

从来没有想过这样做。在我们的测试中,大多数测试都会创建一个用户“ x”,尽管这是唯一的。一次创建数据库将意味着更改测试以重复使用这些对象。
卡拉

我知道,我们在同一页面上,我喜欢您的方法。您的方法保证了每个测试用例都可以独立运行,而不必考虑顺序,并且每次运行之前,数据表的状态都相同。
Downhillski '18

没错,顺序不重要。我们之前已经看到测试失败,因为构建PC和本地计算机上的单元测试运行顺序不同。
卡拉
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.