it-swarm.cn

使用MongoDB进行单元测试

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

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

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

我真的需要某种模拟机制,但是,我对模拟框架没有多少经验,而且Google似乎没有返回任何可能与MongoDB一起使用的模拟框架。

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

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

56
wulfgarpro

正如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));

这也是一种选择。当然,模拟的创建和相应对象的返回仅作为上面的例子编码。

28
rit

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

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

在测试与Mongo(例如MongoUserService)对话的服务的实现时,最简单的方法是编写一些Java代码来启动/停止本地机器上的mongo进程,并让你的MongoUserService连接到它,请参阅 一些笔记的问题

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

52
sbridges

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

今天我认为最好的做法是在Python上使用 testcontainers library(Java)或 testcontainers-python port。它允许使用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)
6
Eugene Lopatkin

我很惊讶没有人建议使用 fakemongo 到目前为止。它很好地模拟了mongo客户端,并且它都在相同的JVM上运行测试 - 因此集成测试变得强大,并且在技术上更接近于真正的“单元测试”,因为没有发生外部系统交互。这就像使用嵌入式H2对您的SQL代码进行单元测试一样。我很高兴在单元测试中使用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
}

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

1
int21h