76

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

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

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

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

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

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

4

5 回答 5

71

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

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

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

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

于 2011-11-25T06:13:41.057 回答
34

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

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

于 2011-11-25T07:29:16.390 回答
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();
于 2013-02-20T17:29:49.633 回答
11

今天我认为最好的做法是在 Python 上使用testcontainers库(Java)或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)
于 2018-10-30T06:27:20.020 回答
3

我很惊讶到目前为止没有人建议使用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
}

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

于 2018-03-20T21:32:17.460 回答