如何模拟一个用于测试的数据库(Java)?

85
我在使用Java进行编程,我的应用程序大量使用DB。因此,能够轻松地测试我的DB使用情况对我来说非常重要。 DB测试的目的是什么?对我而言,它们应该满足两个简单的要求:
1. 验证SQL语法。 2. 更重要的是,根据给定情况检查数据是否被正确地选择/更新/插入。
虽然我需要一个DB,但实际上我更喜欢不使用DB进行测试,原因如下:
1. 在我的工作场所,要拥有一个个人测试DB几乎是不可能的,你必须使用“公共”DB,每个人都可以访问。 2. DB测试往往比通常的测试慢。拥有慢速测试并不理想。 3. 对于每个情况都尝试模拟和DB中一样的操作是繁琐且耗时的。 4. 使用DB时,测试通常与核心代码完全相同。
因此,我正在寻找一种模拟DB的方式,使用文件系统或虚拟内存。我认为可能有一个Java工具/包,允许我在每个测试中使用代码接口简单地构造一个DB模拟,带有模拟表和行、SQL验证以及用于监视其状态的代码接口。您了解这种工具吗?
public class TestDBMonitor extends TestCase {

    @Override
    public void setUp() throws Exception {

       MockConnection connection = new MockConnection();

       this.tableName = "table1";
       MockTable table = new MockTable(tableName);

       String columnName = "column1";
       ColumnType columnType = ColumnType.NUMBER;
       int columnSize = 50;
       MockColumn column = new MockColumn(columnName, columnType, columnSize);
       table.addColumn(column);

       for (int i = 0; i < 20; i++) {
           HashMap<MockColumn, Object> fields = new HashMap<MockColumn, Object>();
           fields.put(column, i);
           table.addRow(fields);
       }

       this.connection = connection;
    }

    @Test
    public void testGatherStatistics() throws Exception {

       DBMonitor monitor = new DBMonitor(connection);
       monitor.gatherStatistics();
       assertEquals(((MockConnection) connection).getNumberOfRows(tableName),
                    monitor.getNumberOfRows(tableName));
    }

    String tableName;
    Connection connection;
}

希望这段代码足够清晰,能够理解我的想法(抱歉语法错误,我是手动打字的,没有我的亲爱的Eclipse:P)。

顺便说一下,我部分使用ORM,并且我的原始SQL查询非常简单,不应该在不同平台上有所不同。

15个回答

42

Java自带Java DB

然而,我建议除非你经过ORM层,否则不要使用与生产环境不同类型的数据库。否则,你的SQL可能没有你想象的那么跨平台。

还可以查看DbUnit


Java 9+ 不包含 Java DB,并且已从 Java 7 和 8 中移除。 - Martin

35

对旧问题的新回答(但事情已经有所进展):

如何模拟测试用的数据库(Java)?

不要模拟,而是模仿您的存储库,并且不对其进行测试,或者在测试中使用相同的数据库并测试SQL。所有内存数据库都不完全兼容,因此它们无法为您提供全面的覆盖范围和可靠性。永远不要尝试模拟/模拟深层次的数据库对象,例如连接、结果集等,这根本没有任何价值,而且开发和维护起来非常麻烦。

拥有个人测试DB几乎是不可能的。您必须使用“公共”数据库,每个人都可以访问

不幸的是,很多公司仍然使用这种模式,但现在我们有了docker,几乎每个数据库都有镜像。商业产品有一些限制(如最多几GB的数据),对于测试来说这些限制并不重要。另外,您需要在此本地数据库上创建您的模式和结构。

“这些测试肯定不快……”- DB测试往往比普通测试慢。拥有慢速的测试真的不理想。

是的,DB测试较慢,但它们并不是非常慢。我进行了一些简单的测量,一个典型的测试需要5-50ms。需要时间的是应用程序启动。有很多方法可以加快这个过程:

  • 首先,依赖注入框架(如Spring)提供了一种运行应用程序某些部分的方法。如果您编写应用程序时具有良好的数据库和非数据库相关逻辑分离,则在测试中可以仅启动数据库部分
  • 每个数据库都有很多调整选项,可以使其更快但不太持久,这非常适合测试。 Postgres 示例
  • 您还可以将整个数据库放入 tmpfs 中

  • 另一个有用的策略是将测试分组,并默认关闭数据库测试(如果它们真正减慢了构建速度)。这样,如果某人实际在处理数据库,则需要在命令行中传递附加标记或使用 IDE (testng groups 和自定义测试选择器非常适用于此)

对于每种情况,应该进行一定量的插入/更新查询,这很烦人且耗费时间

上面已经讨论了“耗费时间”的部分。那么它是否烦人呢?我看到两种方式:

  • 为所有测试用例准备一个数据集。然后你必须维护它并考虑它。通常它与代码分离。它具有千字节或兆字节。它太大了,无法在一个屏幕上查看、理解和思考。它会引入测试之间的耦合性。因为当您需要为测试 A 添加更多行时,测试 B 中的 count(*) 将失败。它只会不断增长,因为即使您删除了某些测试,也不知道哪些行仅由此测试使用
  • 每个测试准备自己的数据。这样每个测试都是完全独立的、可读的和易于理解的。它是否令人厌烦?在我看来,一点也不!它让你非常快地编写新的测试,并在未来节省大量工作

"你怎么知道那张表中有542行?" - 测试的主要原则之一是以不同于被测试代码的方式测试其功能

嗯...不完全是。主要原则是检查您的软件是否针对特定输入生成所需的输出。因此,如果您调用dao.insert 542次,然后您的dao.count返回542,则意味着您的软件按照规定运行。如果需要,您可以在中间调用commit/drop cache。当然,有时您想测试实现而不是合同,然后检查dao是否更改了数据库的状态。但是您始终使用sql B(insert vs select、sequence next_val vs returned value等)测试sql A。是的,您总是会遇到问题“谁来测试我的测试”,答案是:没有人,所以让它们简单!

其他可能帮助您的工具:

  1. testcontainers将帮助您提供真实的数据库。

  2. dbunit - 将帮助您在测试之间清理数据

    缺点:

    • 需要大量工作来创建和维护模式和数据,特别是当您的项目处于密集开发阶段时
    • 这是另一个抽象层,因此,如果突然您想使用某些不受此工具支持的db功能,测试可能会很困难
  3. testegration - 旨在为您提供完整、可用和可扩展的生命周期(声明:我是创建者)。

    缺点:

    • 仅适用于小型项目的免费版
    • 非常年轻的项目
  4. FlywayLiquibase - db迁移工具。它们可以帮助您轻松地为测试创建本地数据库上的模式和所有结构。


9
我必须得承认,我没想到会有人重新审视这个问题并费心写一个更新的答案。8年前我提出了这个问题,自那以后我积累了更多实践经验,现在大部分都同意你的回答 - 特别是关于“用同样的代码测试功能”的那一部分。 - Eyal Roth
每个测试都准备自己的数据。这样每个测试都是完全独立的,可读性强,易于推理。这是不是很烦人?是的,如果你需要准备的记录有上百条,那就很烦人。我会说这是一个权衡。如果集成测试所需的数据在数据库中很少,那么可以在测试用例中处理。否则,可以使用插入到测试容器中的归档文件来处理。编写测试数据的优点是,如果数据库发生变化,更容易维护。使用归档文件的优点是,集成测试的设置代码更少。 - undefined

11

我曾经使用过Hypersonic来实现这个目的。基本上,它是一个JAR文件(纯Java内存数据库),可以在其自己的JVM或您自己的JVM中运行,在其运行时,您就有了一个数据库。然后停止它,你的数据库就消失了。到目前为止,我已将其仅用作内存数据库。当运行单元测试时,通过Ant启动和停止非常简单。


11

如何测试数据库连接(例如 SQL)等集成点有许多观点。我的个人规则如下:

1) 将数据库访问逻辑和函数与一般业务逻辑分开,并在其后面隐藏一个接口。 原因 1:为了测试系统中的绝大部分逻辑,最好使用一个虚拟/存根代替实际数据库,因为它更简单。 原因 2:速度更快。

2) 将数据库的测试视为独立于单元测试主体的集成测试,并且需要在设置的数据库上运行。 原因:测试速度和质量。

3) 每个开发人员都需要自己独立的数据库。他们需要一种自动化的方式来根据团队成员的更改更新其结构并引入数据。请参见第4和第5点。

4) 使用像http://www.liquibase.org这样的工具来管理数据库结构升级。 原因:使您能够灵活地更改现有结构并向前移动版本。

5) 使用像http://dbunit.sourceforge.net/这样的工具来管理数据。为特定的测试用例设置场景文件(xml或XLS)和基础数据,并仅清除任何一个测试用例所需的数据。 原因 1:比手动插入和删除数据更好。 原因 2:更容易让测试人员了解如何调整场景。 原因 3:执行速度更快。

6) 您需要具有类似 DBUnit 场景数据的功能测试,但这些数据集较大,并且会执行整个系统。这完成了将以下知识组合起来的步骤: a)单元测试运行,因此逻辑正确 b)与数据库的集成测试运行,SQL 正确 从而导致“整个系统作为顶部到底部堆栈一起工作”。

到目前为止,这种组合对我实现高质量的测试和产品以及保持单元测试开发速度和更改灵活性非常有帮助。


8
“只需要自己搭建一个测试数据库,有多难?”——在我的工作场所,拥有个人的测试数据库是非常困难的。你必须使用“公共”数据库,这意味着每个人都可以访问。

听起来你在工作中遇到了文化问题,这妨碍了你充分发挥能力并为产品做出贡献。你可能需要采取一些行动。

另一方面,如果您的数据库模式已经进行版本控制,那么您始终可以创建一个测试构建,该构建从模式创建数据库,使用测试数据填充它,运行测试,收集结果,然后删除数据库。它只存在于测试期间。如果硬件是个问题,它可以是现有安装中的新数据库。这类似于我们在我工作的地方所做的事情。


6

如果你在工作中使用Oracle,你可以使用闪回数据库功能中的还原点来使数据库返回到测试前的某个时间点。这将清除你个人对数据库所做的任何更改。

参见:

https://docs.oracle.com/cd/E11882_01/backup.112/e10642/flashdb.htm#BRADV71000

如果您需要用于Oracle生产/工作的测试数据库,则可以查找来自Oracle的XE Express Edition数据库。这是免费供个人使用的,数据库大小限制为小于2GB。

3
我们最近转而使用JavaDB或Derby来实现这一点。 Derby 10.5.1.1现在实现了内存表示,因此运行非常快,不需要访问磁盘: Derby In Memory Primer 我们设计我们的应用程序可以运行在Oracle、PostgreSQL和Derby上,这样我们就不会在发现一个数据库支持其他数据库不支持的功能之前,在任何一个平台上走得太远。

1

jOOQ是一个工具,除了提供SQL抽象外,还内置了一些小工具,例如SPI,可以模拟JDBC的全部功能。这篇博客文章记录了两种使用方式:

通过实现MockDataProvider SPI:

// context contains the SQL string and bind variables, etc.
MockDataProvider provider = context -> {

    // This defines the update counts, result sets, etc.
    // depending on the context above.
    return new MockResult[] { ... }
};

在上述实现中,您可以通过编程方式拦截每个SQL语句并为其返回结果,甚至可以通过“解析”SQL字符串来提取一些谓词/表信息等动态地返回结果。
通过使用更简单(但功能较弱)的MockFileDatabase,其格式如下所示(一组语句/结果对):
select first_name, last_name from actor;
> first_name last_name
> ---------- ---------
> GINA       DEGENERES
> WALTER     TORN     
> MARY       KEITEL   
@ rows: 3

上述文件可以按以下方式读取和使用:
import static java.lang.System.out;
import java.sql.*;
import org.jooq.tools.jdbc.*;

public class Mocking {
    public static void main(String[] args) throws Exception {
        MockDataProvider db = new MockFileDatabase(
            Mocking.class.getResourceAsStream("/mocking.txt");

        try (Connection c = new MockConnection(db));
            Statement s = c.createStatement()) {

            out.println("Actors:");
            out.println("-------");
            try (ResultSet rs = s.executeQuery(
                "select first_name, last_name from actor")) {
                while (rs.next())
                    out.println(rs.getString(1) 
                        + " " + rs.getString(2));
            }
        }
    }
}

请注意,我们正在直接使用JDBC API,而不实际连接到任何数据库。
请注意,我为jOOQ供应商工作,因此这个答案是有偏见的。
请注意,在某些时候,您正在实现整个数据库
上述方法适用于简单情况。但请注意,最终您将要实现整个数据库,您需要:
  1. 验证SQL语法。
通过上面展示的模拟数据库,您可以"验证"语法,因为您没有事先预见到的每种语法在任何这样的模拟方法中都将被拒绝。
您可以实现解析器来解析SQL(或者,再次使用jOOQ),然后将SQL语句转换成更容易识别和生成结果的形式。 但归根结底,这只意味着实现了整个数据库。
  1. 更重要的是,根据给定情况正确地选择/更新/插入数据。

这使得事情变得更加困难。如果您先运行插入然后更新,结果显然与先更新再插入不同,因为更新可能会影响插入的行。

在“模拟”数据库时,如何确保发生这种情况?您需要一个状态机来记住每个“模拟”表的状态。换句话说,您将实现一个数据库。

模拟只能带你到这里

正如piotrek所提到的,模拟只能带你到这里。它在简单情况下很有用,当您只需要拦截几个非常熟知的查询时。如果要为整个系统模拟数据库,则不可能。在这种情况下,请使用实际数据库,最好是您在生产中使用的同一产品。


1

尝试使用derby。它易于使用且便携。使用Hibernate使您的应用程序变得灵活。在derby上进行测试,在任何您喜欢和信任的生产环境中运行。


1
我认为我的Acolyte框架可以用于这样的数据库模拟:https://github.com/cchantep/acolyte
它允许使用现有的Java代码(用于测试),并处理您查询/更新的连接:根据执行情况返回适当的结果集、更新计数或警告。

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