使用 MongoDB 进行单元测试

Posted

技术标签:

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

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

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

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

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

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

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

【问题讨论】:

【参考方案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();

【讨论】:

假的,不是存根。 Using "mongo-java-server-1.39.0.jar" as github.com/bwaldvogel/mongo-java-server#readme can't run on Java 17 using IntelliJ with junit 5.8,2 给出:java.lang.NullPointerException:无法调用“com.mongodb.client.MongoCollection.countDocuments()”因为“this.collection”为空 @NOTiFY:您能否通过github.com/bwaldvogel/mongo-java-server/issues 提交带有可重现单元测试的错误报告?【参考方案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-unit、jsonunit 等,您可以获得涵盖 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

以上是关于使用 MongoDB 进行单元测试的主要内容,如果未能解决你的问题,请参考以下文章

在 NestJS 中使用 Jest 和 MongoDB 进行单元测试

如何模拟 mongodb 以进行单元测试 graphql 解析器

在 Grails 2.2 中是不是可以对 mongodb 动态属性进行单元测试?

MongoDB / Mongoose 单元测试 - 最佳实践? [关闭]

MongoDB 单元测试

NodeJS 和 MongoDB 单元测试