如何模拟数据库进行测试(Java)?

时间:2009-05-30 01:53:43

标签: java database unit-testing testing jdbc

我正在用Java编程,我的应用程序正在大量使用DB。因此,能够轻松测试我的数据库使用情况对我来说很重要 DB测试的全部内容是什么?对我来说,他们应该提供两个简单的要求:

  1. 验证SQL语法。
  2. 更重要的是,根据给定的情况检查数据是否被正确选择/更新/插入。
  3. 那么,似乎我所需要的只是一个数据库 但实际上,我不喜欢,因为使用数据库进行测试的困难很少:

    • “只是让自己成为一名测试数据库,它有多难?” - 嗯,在我的工作场所,拥有个人测试数据库是非常不可能的。您必须使用“公共”数据库,每个人都可以访问。
    • “这些测试肯定不会很快......” - DB测试往往比通常的测试慢。慢慢测试真的不太理想。
    • “这个程序应该处理任何情况!” - 尝试模拟数据库中的每个案例变得有些烦人甚至无法实现。对于每种情况,都应该进行一定量的插入/更新查询,这很烦人并且需要时间。
    • “等一下,你怎么知道该表中有542行?” - 测试的主要原则之一是能够以与测试代码不同的方式测试功能。使用数据库时,通常有一种方法可以做某事,因此测试与核心代码完全相同。

    所以,你可以看出我在测试时不喜欢数据库(当然我必须在某些方面达到这个目的,但是在我发现大多数之后我宁愿在我的测试后到达那里)使用其余测试方法的错误)。但我在寻找什么?

    我正在寻找一种使用文件系统或虚拟内存来模拟数据库,模拟数据库的方法。我想也许有一个Java工具/包允许简单构建(使用代码接口)每个测试的数据库模拟,模拟表和行,SQL验证,以及用于监视其状态的代码接口(而不是使用SQL) )。

    您熟悉这种工具吗?


    编辑:感谢您的回答!虽然我要求提供一个工具,但你也向我提供了一些关于这个问题的提示:)我需要一些时间来查看你的报价,所以我现在不能说你的答案是否令人满意。

    无论如何,这里是我正在寻找的更好的视图 - 想象一个名为DBMonitor的类,它的一个功能是查找表中的行数。这是一个虚构的代码,说明如何使用JUnit测试该功能:

    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 个答案:

答案 0 :(得分:40)

Java附带Java DB

那就是说,除非你通过ORM层,否则我建议不要使用不同于生产中使用的DB。否则,您的SQL可能不像您想象的那样跨平台。

另请查看DbUnit

答案 1 :(得分:14)

旧问题的新答案(但事情已向前推进了一点):

  

如何模拟数据库进行测试(Java)?

你没有模拟它。你嘲笑你的存储库,你不测试它们,或者你在测试中使用相同的数据库并测试你的sqls。所有内存中的dbs都不完全兼容,因此它们不会为您提供全面的覆盖和可靠性。从来没有尝试模拟/模拟深度数据库对象,如连接,结果集等,它根本没有给你任何价值,是开发和维护的噩梦

  

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

不幸的是,许多公司仍然使用该模型,但现在我们有docker并且几乎每个数据库都有图像。商业产品有一些限制(如高达几gb的数据)对测试不重要。您还需要在此本地数据库上创建模式和结构

  

“这些测试肯定不会很快......” - DB测试往往比平常测试慢。慢速测试真的不太理想。

是的,db测试速度较慢,但​​速度并不慢。我做了一些简单的measurements,典型的测试耗时5-50毫秒。什么需要时间是应用程序启动。有很多方法可以加快速度:

  • 第一个DI框架(如spring)提供了一种只运行应用程序某些部分的方法。如果您编写的应用程序具有良好的db和非db相关逻辑分离,那么在您的测试中,您可以start only the db part
  • 每个数据库都有大量的调优选项,使其不那么耐用,而且速度更快。这非常适合测试。 postgres example
  • 您也可以将整个数据库放入tmpfs

  • 另一个有用的策略是让测试组和默认情况下关闭数据库测试(如果它们确实减慢了你的构建速度)。这样,如果有人正在使用db,他需要在cmd行中传递额外的标志或使用IDE(testng组和自定义测试选择器是完美的)

  

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

上面讨论了

'需要时间'部分。这很烦人吗?我见过两种方式:

  • 为您的所有测试用例准备一个数据集。然后你必须维护它并推理它。通常它与代码分开。它有千字节或兆字节。在一个屏幕上看到,理解和推理是很重要的。它引入了测试之间的耦合。因为当您需要更多行进行测试A时,测试B中的count(*)会失败。它只会增长,因为即使你删除了一些测试,你也不知道哪一行只被这一个测试使用
  • 每个测试准备其数据。这样每个测试都是完全独立,可读且易于推理的。这很烦人吗?我没有,完全没有!它可以让你快速编写新的测试并为将来的工作节省很多工作
  

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

嗯...不是真的。主要原则是检查您的软件是否根据特定输入生成所需的输出。因此,如果您拨打dao.insert 542次,然后dao.count返回542,则表示您的软件按指定工作。如果需要,可以在两者之间调用commit / drop cache。当然,有时你想测试你的实现而不是合同,然后检查你的dao是否改变了db的状态。但是你总是使用sql B测试sql A(插入vs选择,序列next_val vs返回值等)。是的,你总会遇到“谁将测试我的测试”的问题,答案是:没有人,所以保持简单!

可能对您有所帮助的其他工具:

  1. testcontainers会帮助您提供 真正的数据库。

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

    缺点:

    • 创建和维护架构和数据需要做很多工作。特别是当您的项目处于密集开发阶段时。
    • 它是另一个抽象层,所以如果突然想要使用这个工具不支持的某个数据库功能,可能很难对其进行测试
  3. testegration - 旨在为您提供完整,随时可用且可扩展的生命周期(披露:我是创建者)。

    缺点:

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

答案 2 :(得分:10)

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

答案 3 :(得分:10)

关于如何通过SQL测试集成点(如数据库连接)有很多观点。我个人的规则对我有用,如下:

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

2)将数据库的测试视为与单元测试主体分离并需要在设置数据库上运行的集成测试 原因:测试的速度和质量

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

4)使用http://www.liquibase.org之类的工具来管理数据库结构中的升级。 原因:使您能够灵活地更改现有结构并在版本中继续前进

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

6)你需要功能测试,它也有类似场景数据的DBUnit,但这是更大的数据集并执行整个系统。这完成了结合知识的步骤 a)单元测试运行,因此逻辑是合理的 b)对数据库运行和SQL的集成测试是正确的 导致“和整个系统一起作为一个从上到下的堆栈”

迄今为止,这种组合对于实现高质量的测试和产品以及保持单元测试开发的速度和改变的灵活性非常有用。

答案 4 :(得分:6)

  

“只是让自己成为一名测试数据库,它有多难?” - 嗯,在我的工作场所,拥有个人测试数据库是非常不可能的。您必须使用“公共”数据库,每个人都可以访问。

听起来你在工作中遇到了文化问题,这些问题为你能够充分利用自己的能力和产品的利益提供了障碍。你可能想对此做些什么。

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

答案 5 :(得分:4)

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

请参阅:

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

如果您需要一个用于Oracle生产/工作的测试数据库,那么从Oracle查找XE,express版本数据库。这是免费供个人使用,数据库限制小于2GB。

答案 6 :(得分:3)

我们最近切换到JavaDB或Derby来实现这一点。 Derby 10.5.1.1现在实现了一个内存中表示,因此运行速度非常快,不需要转到磁盘: Derby In Memory Primer

我们将我们的应用程序设计为在Oracle,PostgreSQL和Derby上运行,因此在发现一个数据库支持其他数据库不支持的功能之前,我们不会在任何一个平台上走得太远。

答案 7 :(得分:1)

我们现在正在创建一个数据库测试环境。我们认为必须将真正的数据库管理系统与模拟数据一起使用。模拟DBMS的一个问题是SQL从未真正完全凝固为标准,因此人工测试环境必须忠实地支持我们的生产数据库方言。另一个问题是我们广泛使用列值约束,外键约束和唯一约束,并且因为人工工具可能不会实现这些,我们的单元测试可能会通过,但我们的系统测试在它们第一次触及真实时会失败限制。如果测试时间过长,则表明实现错误,我们会调整查询(通常测试数据集与生产相比微不足道)。

我们在每台开发人员机器上以及我们的持续集成和测试服务器上安装了一个真正的DBMS(我们使用Hudson)。我不知道你的工作策略限制是什么,但是安装和使用PostgreSQL,MySQL和Oracle XE非常容易。这些都是免费的开发用途(甚至是Oracle XE),所以没有合理的理由禁止它们使用。

关键问题是如何保证测试始终始于数据库处于一致状态?如果测试都是只读的,没问题。如果您可以设计变异测试以始终在永不提交的事务中运行,那么没问题。但通常您需要担心撤消更新。为此,您可以将初始状态导出到文件,然后在测试后将其导回(Oracle的exp和imp shell命令执行此操作)。或者您可以使用检查点/回滚。但更优雅的方法是使用像dbunit这样的工具,这对我们很有用。

这样做的关键优势在于,我们可以预先捕获更多错误,这些错误更容易修复,而且我们的实际系统测试不会被阻止,而开发人员会疯狂地尝试调试问题。这意味着我们可以更快,更省力地生成更好的代码。

答案 8 :(得分:1)

我同意banjollity。建立孤立的开发和测试环境应该是一个高优先级。我使用的每个数据库系统都是开源的,或者是可以在本地工作站上安装的免费开发人员版本。这使您可以使用与生产相同的数据库方言进行开发,为您提供对开发数据库的完全管理访问权限,并且比使用远程服务器更快。

答案 9 :(得分:1)

尝试使用derby。它简单易用。使用Hibernate,您的应用程序变得灵活。测试德比,生产任何你喜欢和信任的东西。

答案 10 :(得分:1)

我认为我的Acolyte框架可用于此类数据库模拟:https://github.com/cchantep/acolyte

它允许使用您查询/更新处理的连接运行现有Java(用于测试):根据执行情况返回适当的结果集,更新计数或警告。

答案 11 :(得分:1)

您可以在内存数据库测试中使用HSQLDB。启动内存数据库并在其上运行测试非常简单 http://hsqldb.org/

答案 12 :(得分:0)

首先,您使用任何ORM层进行数据库访问吗?
如果不是:那么你所想的就没用了。当你不确定你正在解雇的SQL是否会在生产中使用你的数据库时,测试的用途是什么,就像你正在使用其他东西的测试用例一样。
如果是:那么你可以看看指出的各种选项。

答案 13 :(得分:0)

jOOQ是一个工具,除了提供SQL抽象之外,还有一些内置的小工具,例如SPI,可以模拟整个JDBC。 This can work in two ways as documented in this blog post

通过实施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语法。
  2.   

好的,通过如上所示模拟数据库,您可以“验证”语法,因为您在上面列出的 exact 版本中没有预见到的每种语法都会被任何此类模拟拒绝方法

您可以实现解析SQL(or, again, use jOOQ's)的解析器,然后将SQL语句转换为您可以更容易识别并生成结果的内容。但最终,这只意味着实现整个数据库。

  
      
  1. 更重要的是,根据给定的情况检查数据是否被正确选择/更新/插入。
  2.   

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

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

Mocking只会带你到这个

正如piotrek提到的那样,嘲笑只会把你带到这一步。当您需要拦截几个众所周知的查询时,它在简单的情况下很有用。如果你想模拟整个系统的数据库是不可能的。在这种情况下,请使用实际的数据库,理想情况下是您在生产中使用的相同产品。

答案 14 :(得分:0)

H2 数据库

“许多 Java 开发人员最喜欢的集成测试数据库。”

(Hysonic变成HSQLDB改写为H2)

https://blog.jooq.org/2015/08/18/jooq-tuesdays-thomas-muller-unveils-how-hsqldb-evolved-into-the-popular-h2-database/