使用MongoDB进行单元测试

77

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

我在按照TDD方式逐步测试我的逻辑。在每个单元测试之前,会调用一个@Before方法来创建一个数据库单例,在测试完成后,将调用@After方法来删除数据库。这有助于促进单元测试的独立性。

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

实际上,我需要一些模拟机制,但是我没有太多使用模拟框架的经验,而且似乎Google返回的内容中没有与MongoDB一起使用的模拟框架。

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

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

5个回答

75

从技术上讲,与数据库(nosql或其他类型)交互的测试不是单元测试,因为这些测试涉及与外部系统的交互,而不仅仅是测试独立的代码单元。然而,与数据库交互的测试通常非常有用,并且通常足够快速,可以与其他单元测试一起运行。

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

在测试与Mongo交互的服务实现时(例如MongoUserService),最简单的方法是编写一些Java代码,在本地计算机上启动/停止Mongo进程,并让MongoUserService连接到该进程,有关详细信息,请参见此问题的说明

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


3
希望现在有人正在发明一个类似于 DbUnit 的 MongoDB 框架—— MongoUnit... - Raman
1
我还没有尝试过这个,但是可以看看这个链接:https://github.com/lordofthejars/nosql-unit - Raman
非常有用的答案。不过,其中对DbUnit的引用有点误导人:因为目前DbUnit仅支持关系型数据库,不支持MongoDB。我加入@Raman的愿望 :) - Taoufik Mohdit
@Raman 请查看 https://mongoUnit.org。 - user1902183

36

正如sbridges在这篇文章中所写,没有一个专门的服务(有时也称为存储库或DAO)来将数据访问与逻辑分离是一个不好的想法。然后你可以通过提供一个DAO的模拟来测试逻辑。

另一个我使用的方法是创建一个Mongo对象的模拟(例如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));

那也是一个选择。当然,上面编写的模拟和返回相应对象的代码只是一个示例。


1
你需要在这里使用PowerMockito吗?看起来直接使用Mockito(或EasyMock)就可以完成这个任务。 - Raman
没错,你说得对。Mockito就足够了。我们在几个地方使用PowerMockito,这就是为什么我只是用PowerMockito写了一个例子。使用Mockito也应该没问题。 - rit
@Raman,我认为你的评论是错误的。我们发现Mongo API使用了很多final关键字,并且不允许我们对findOne方法进行存根。对于我们来说,只有PowerMockito方法可行。 - cburgmer
1
还要注意,从Mongo 3.x开始,getDB()和getCollection()已被弃用,因此您需要执行类似以下操作:MongoClient mongo = Mockito.mock(MongoClient.class); MongoDatabase db = Mockito.mock(MongoDatabase.class); MongoCollection dbCollection = Mockito.mock(MongoCollection.class); - panza

22

我用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();

1
假的,不是存根。 - wulfgarpro
1
使用“mongo-java-server-1.39.0.jar”作为https://github.com/bwaldvogel/mongo-java-server#readme无法在Java 17上运行,使用IntelliJ和junit 5.8,2会出现以下错误:java.lang.NullPointerException: Cannot invoke "com.mongodb.client.MongoCollection.countDocuments()" because "this.collection" is null。 - NOTiFY
@NOTiFY:您能否通过https://github.com/bwaldvogel/mongo-java-server/issues提交一个可重现的单元测试的错误报告? - Benedikt Waldvogel

13

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

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 (示例):

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)

3
我认为这是最好的方式。有了Docker,我们不再需要任何模拟服务器,并且可以进行自动化测试。 - Lewis Chan

4
我很惊讶到目前为止没有人建议使用fakemongo。它可以很好地模拟Mongo客户端,所有内容都在同一个JVM上运行,并且具有强大的集成测试功能,从技术上讲更接近于真正的"单元测试",因为不存在外部系统交互。这就像使用嵌入式H2来对SQL代码进行单元测试。 在测试Spring上下文中考虑以下配置,我非常满意在端到端方式中测试数据库集成代码时使用fakemongo进行单元测试。
@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)。


这些类型的伪造并不特别健壮,因为它们可以与真实实现相差很大。 - wulfgarpro
比起在真实的集成数据库实例上运行,这仍然更快更简单,对吧? :) - int21h
如果你使用小型测试数据集,数据库交互代码所需时间远少于Spring测试上下文初始化。在一些项目中,我见过这种类型的测试通过只需要100-200毫秒,还不错。它可以节省很多开发工作量,因为你不需要在尝试特定测试的情况下正确模拟你的服务。 - int21h
很遗憾,由于https://github.com/fakemongo/fongo/issues/316的问题,在2021年它无法工作。 - Anton

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