【问题标题】:Unit testing with MongoDB使用 MongoDB 进行单元测试
【发布时间】:2011-11-16 20:35:31
【问题描述】:

我选择的数据库是 MongoDB。我正在编写一个数据层 API 来从客户端应用程序中抽象实现细节——也就是说,我本质上是提供一个公共接口(一个充当 IDL 的对象)。

我正在以 TDD 方式测试我的逻辑。在每个单元测试之前,调用@Before 方法来创建数据库单例,之后,当测试完成时,调用@After 方法来删​​除数据库。这有助于促进单元测试之间的独立性。

几乎所有单元测试,即执行上下文查询,都需要事先发生某种插入逻辑。我的公共接口提供了一个插入方法 - 但是,将此方法用作每个单元测试的前导逻辑似乎是不正确的。

我确实需要某种模拟机制,但是,我在模拟框架方面没有太多经验,而且 Google 似乎没有返回任何可能用于 MongoDB 的模拟框架。

其他人在这些情况下会怎么做?也就是说,人们如何对与数据库交互的代码进行单元测试?

此外,我的公共接口连接到在外部配置文件中定义的数据库 - 使用此连接进行单元测试似乎不正确 - 再次,这种情况会从某种模拟中受益?

【问题讨论】:

    标签: java unit-testing mongodb junit morphia


    【解决方案1】:

    从技术上讲,与数据库(nosql 或其他)对话的测试不是unit tests,因为测试是测试与外部系统的交互,而不仅仅是测试一个孤立的代码单元。但是,与数据库通信的测试通常非常有用,并且通常足够快,可以与其他单元测试一起运行。

    通常我有一个服务接口(例如UserService),它封装了处理数据库的所有逻辑。依赖 UserService 的代码可以使用 UserService 的模拟版本并且易于测试。

    在测试与 Mongo 对话的 Service 的实现时(例如 MongoUserService),最简单的方法是编写一些 java 代码来启动/停止本地机器上的 mongo 进程,并让 MongoUserService 连接到该进程,请参阅此question for some notes

    您可以在测试 MongoUserService 时尝试模拟数据库的功能,但通常这样太容易出错,并且不会测试您真正想要测试的内容,即与真实数据库的交互。因此,在为 MongoUserService 编写测试时,您需要为每个测试设置一个数据库状态。查看DbUnit 以获取使用数据库执行此操作的框架示例。

    【讨论】:

    • 希望有人正在为 MongoDB 发明一个类似 DbUnit 的框架——MongoUnit——现在......
    • 我还没有尝试过,但请查看:github.com/lordofthejars/nosql-unit
    • 非常有用的答案。我不得不承认,其中对 DbUnit 的引用有点误导:因为 DbUnit 目前仅支持关系数据库,不支持 MongoDB。我希望加入@Raman :)
    【解决方案2】:

    正如 sbridges 在这篇文章中所写,不提供从逻辑中抽象数据访问的专用服务(有时也称为存储库或 DAO)是一个坏主意。然后,您可以通过提供 DAO 的模拟来测试逻辑。

    我做的另一种方法是创建 Mongo 对象的 Mock(例如 PowerMockito),然后返回适当的结果。 这是因为您不必测试数据库是否在单元测试中工作,但更多的是您应该测试是否将正确的查询发送到数据库。

    Mongo mongo = PowerMockito.mock(Mongo.class);
    DB db = PowerMockito.mock(DB.class);
    DBCollection dbCollection = PowerMockito.mock(DBCollection.class);
    
    PowerMockito.when(mongo.getDB("foo")).thenReturn(db);
    PowerMockito.when(db.getCollection("bar")).thenReturn(dbCollection);
    
    MyService svc = new MyService(mongo); // Use some kind of dependency injection
    svc.getObjectById(1);
    
    PowerMockito.verify(dbCollection).findOne(new BasicDBObject("_id", 1));
    

    这也是一种选择。当然,模拟的创建和适当对象的返回只是作为上面的示例进行编码。

    【讨论】:

    • 这里需要 PowerMockito 吗?在这种情况下,看起来直接的 Mockito(或 EasyMock)可以完成这项工作。
    • 是的,你是对的。 Mockito 就足够了。我们在几个地方使用 PowerMockito,这就是我刚刚用 PowerMockito 编写示例的原因。 Mockito 也应该没问题。
    • @Raman,我相信你的评论是错误的。我们发现 Mongo API 大量使用 final 并且不允许我们存根例如 findOne 方法。对我们来说,只有 PowerMockito 方法有效。
    • 另请注意,从 Mongo 3.x 开始,getDB() 和 getCollection() 已被弃用,因此您需要执行以下操作: MongoClient mongo = Mockito.mock(MongoClient.class); MongoDatabase db = Mockito.mock(MongoDatabase.class); MongoCollection dbCollection = Mockito.mock(MongoCollection.class);
    【解决方案3】:

    我用 Java 写了一个 MongoDB 假实现:mongo-java-server

    默认是内存后端,可以轻松用于单元和集成测试。

    示例

    MongoServer server = new MongoServer(new MemoryBackend());
    // bind on a random local port
    InetSocketAddress serverAddress = server.bind();
    
    MongoClient client = new MongoClient(new ServerAddress(serverAddress));
    
    DBCollection coll = client.getDB("testdb").getCollection("testcoll");
    // creates the database and collection in memory and inserts the object
    coll.insert(new BasicDBObject("key", "value"));
    
    assertEquals(1, collection.count());
    assertEquals("value", collection.findOne().get("key"));
    
    client.close();
    server.shutdownNow();
    

    【讨论】:

    【解决方案4】:

    今天我认为最好的做法是在 Python 上使用 testcontainers 库 (Java) 或 testcontainers-python 端口。它允许使用带有单元测试的 Docker 镜像。 要在 Java 代码中运行容器,只需实例化 GenericContainer 对象 (example):

    GenericContainer mongo = new GenericContainer("mongo:latest")
        .withExposedPorts(27017);
    
    MongoClient mongoClient = new MongoClient(mongo.getContainerIpAddress(), mongo.getMappedPort(27017));
    MongoDatabase database = mongoClient.getDatabase("test");
    MongoCollection<Document> collection = database.getCollection("testCollection");
    
    Document doc = new Document("name", "foo")
            .append("value", 1);
    collection.insertOne(doc);
    
    Document doc2 = collection.find(new Document("name", "foo")).first();
    assertEquals("A record can be inserted into and retrieved from MongoDB", 1, doc2.get("value"));
    

    或在 Python (example) 上:

    mongo = GenericContainer('mongo:latest')
    mongo.with_bind_ports(27017, 27017)
    
    with mongo_container:
        def connect():
            return MongoClient("mongodb://{}:{}".format(mongo.get_container_host_ip(),
                                                        mongo.get_exposed_port(27017)))
    
        db = wait_for(connect).primer
        result = db.restaurants.insert_one(
            # JSON as dict object
        )
    
        cursor = db.restaurants.find({"field": "value"})
        for document in cursor:
            print(document)
    

    【讨论】:

    • 我认为这是最好的方法。有了docker,我们就不需要mock server了,自动测试就可以了。
    【解决方案5】:

    我很惊讶到目前为止没有人建议使用fakemongo。它很好地模拟了 mongo 客户端,并且都运行在带有测试的同一个 JVM 上——因此集成测试变得健壮,并且在技术上更接近真正的“单元测试”,因为没有发生外部系统交互。这就像使用嵌入式 H2 对您的 SQL 代码进行单元测试。 我很高兴在以端到端方式测试数据库集成代码的单元测试中使用 fakemongo。在测试 spring 上下文中考虑这个配置:

    @Configuration
    @Slf4j
    public class FongoConfig extends AbstractMongoConfiguration {
        @Override
        public String getDatabaseName() {
            return "mongo-test";
        }
    
        @Override
        @Bean
        public Mongo mongo() throws Exception {
            log.info("Creating Fake Mongo instance");
            return new Fongo("mongo-test").getMongo();
        }
    
        @Bean
        @Override
        public MongoTemplate mongoTemplate() throws Exception {
            return new MongoTemplate(mongo(), getDatabaseName());
        }
    
    }
    

    有了这个,您可以测试您在 spring 上下文中使用 MongoTemplate 的代码,并结合nosql-unitjsonunit 等,您可以获得涵盖 mongo 查询代码的强大单元测试。

    @Test
    @UsingDataSet(locations = {"/TSDR1326-data/TSDR1326-subject.json"}, loadStrategy = LoadStrategyEnum.CLEAN_INSERT)
    @DatabaseSetup({"/TSDR1326-data/dbunit-TSDR1326.xml"})
    public void shouldCleanUploadSubjectCollection() throws Exception {
        //given
        JobParameters jobParameters = new JobParametersBuilder()
                .addString("studyId", "TSDR1326")
                .addString("execId", UUID.randomUUID().toString())
                .toJobParameters();
    
        //when
        //next line runs a Spring Batch ETL process loading data from SQL DB(H2) into Mongo
        final JobExecution res = jobLauncherTestUtils.launchJob(jobParameters);
    
        //then
        assertThat(res.getExitStatus()).isEqualTo(ExitStatus.COMPLETED);
        final String resultJson = mongoTemplate.find(new Query().with(new Sort(Sort.Direction.ASC, "topLevel.subjectId.value")),
                DBObject.class, "subject").toString();
    
        assertThatJson(resultJson).isArray().ofLength(3);
        assertThatDateNode(resultJson, "[0].topLevel.timestamp.value").isEqualTo(res.getStartTime());
    
        assertThatNode(resultJson, "[0].topLevel.subjectECode.value").isStringEqualTo("E01");
        assertThatDateNode(resultJson, "[0].topLevel.subjectECode.timestamp").isEqualTo(res.getStartTime());
    
        ... etc
    }
    

    我使用 fakemongo 没有问题 mongo 3.4 驱动程序,社区真的很接近发布支持 3.6 驱动程序的版本 (https://github.com/fakemongo/fongo/issues/316)。

    【讨论】:

    • 这些类型的赝品不是特别健壮,因为它们可能与实际实现有很大差异。
    • 仍然比针对真正的集成数据库实例运行更快、更简单,对吧? :)
    • 如果您使用小型测试数据集运行,DB 交互代码所需的时间要少得多,例如 Spring 测试上下文初始化。在少数项目中,我见过这种测试在 100-200 毫秒内通过,还不错。并且它节省了大量的开发工作,因为您无需为特定的测试正确地模拟您的服务。
    • 2021 年它不起作用,不幸的是,因为github.com/fakemongo/fongo/issues/316
    猜你喜欢
    • 2015-03-23
    • 2014-02-26
    • 2018-11-21
    • 1970-01-01
    • 2013-04-01
    • 2018-09-30
    • 1970-01-01
    • 2016-05-30
    • 1970-01-01
    相关资源
    最近更新 更多