web-dev-qa-db-fra.com

Tests unitaires avec MongoDB

Ma base de données de choix est MongoDB. J'écris une API de couche de données pour extraire les détails d'implémentation des applications clientes - c'est-à-dire que je fournis essentiellement une interface publique unique (un objet qui agit comme un IDL).

Je teste ma logique au fur et à mesure d'une manière TDD. Avant chaque test unitaire, un @Before est appelée pour créer un singleton de base de données, après quoi, lorsque le test est terminé, un @After est appelée pour supprimer la base de données. Cela contribue à promouvoir l'indépendance des tests unitaires.

Presque tous les tests unitaires, c'est-à-dire exécution d'une requête contextuelle, nécessitent une sorte de logique d'insertion avant de se produire. Mon interface publique fournit une méthode d'insertion - pourtant, il semble incorrect d'utiliser cette méthode comme logique précurseur pour chaque test unitaire.

Vraiment, j'ai besoin d'une sorte de mécanisme de simulation, pourtant, je n'ai pas beaucoup d'expérience avec les structures de simulation, et il semble que Google ne retourne rien concernant une structure de simulation que l'on pourrait utiliser avec MongoDB.

Que font les autres dans ces situations? Autrement dit, comment les gens testent-ils le code qui interagit avec une base de données?

De plus, mon interface publique se connecte à une base de données définie dans un fichier de configuration externe - il semble incorrect d'utiliser cette connexion pour mes tests unitaires - encore une fois, une situation qui bénéficierait d'une sorte de moquerie?

58
wulfgarpro

Comme sbridges l'a écrit dans ce post, c'est une mauvaise idée de ne pas avoir de service dédié (parfois aussi appelé référentiel ou DAO) qui soustrait l'accès aux données à la logique. Ensuite, vous pouvez tester la logique en fournissant une maquette du DAO.

Une autre approche que je fais consiste à créer une maquette de l'objet Mongo (par exemple PowerMockito), puis à renvoyer les résultats appropriés. Cela parce que vous n'avez pas à tester si la base de données fonctionne dans les tests unitaires, mais plus vous devez tester si la bonne requête a été envoyée à la base de données.

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

Ce serait également une option. Bien sûr, la création des simulations et le retour des objets appropriés sont juste codés comme exemple ci-dessus.

29
rit

Techniquement, les tests qui parlent à une base de données (nosql ou autre) ne sont pas tests unitaires , car les tests testent les interactions avec un système externe, et pas seulement le test d'une unité de code isolée. Cependant, les tests qui parlent à une base de données sont souvent extrêmement utiles et sont souvent assez rapides pour s'exécuter avec les autres tests unitaires.

Habituellement, j'ai une interface de service (par exemple UserService) qui encapsule toute la logique pour traiter la base de données. Le code qui repose sur UserService peut utiliser une version simulée de UserService et est facilement testé.

Lors du test de l'implémentation du service qui parle à Mongo, (par exemple MongoUserService), il est plus facile d'écrire du code Java qui démarrera/arrêtera un processus mongo sur la machine locale et aura votre MongoUserService connectez-vous à cela, voyez ceci question pour quelques notes .

Vous pouvez essayer de simuler les fonctionnalités de la base de données pendant le test de MongoUserService, mais généralement cela est trop sujet aux erreurs et ne teste pas ce que vous voulez vraiment tester, c'est-à-dire l'interaction avec une vraie base de données. Ainsi, lors de l'écriture de tests pour MongoUserService, vous configurez un état de base de données pour chaque test. Regardez DbUnit pour un exemple de framework pour le faire avec une base de données.

61
sbridges

J'ai écrit une fausse implémentation MongoDB en Java: mongo-Java-server

La valeur par défaut est un backend en mémoire, qui peut être facilement utilisé dans les tests unitaires et d'intégration.

Exemple

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

Aujourd'hui, je pense que la meilleure pratique consiste à utiliser testcontainers bibliothèque (Java) ou testcontainers-python port sur Python. Il permet d'utiliser des images Docker avec des tests unitaires. Pour exécuter le conteneur dans Java instancie simplement l'objet GenericContainer ( exemple ):

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"));

ou sur Python ( exemple ):

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)
7
Eugene Lopatkin

Je suis surpris que personne n'ait conseillé d'utiliser fakemongo pour l'instant. Il émule assez bien le client mongo, et tout fonctionne sur la même machine virtuelle Java avec des tests - donc les tests d'intégration deviennent robustes et techniquement beaucoup plus proches des vrais "tests unitaires", car aucune interaction avec un système étranger n'a lieu. C'est comme utiliser le H2 intégré pour tester à l'unité votre code SQL. J'étais très heureux d'utiliser fakemongo dans des tests unitaires qui testent le code d'intégration de base de données de manière complète. Considérez cette configuration dans le contexte du test de printemps:

@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());
    }

}

Avec cela, vous pouvez tester votre code qui utilise MongoTemplate à partir du contexte de printemps, et en combinaison avec nosql-unit , jsonunit , etc. vous obtenez des tests unitaires robustes qui couvrent le code de requête 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
}

J'ai utilisé fakemongo sans problème avec le pilote mongo 3.4, et la communauté est vraiment proche de publier une version qui prend en charge le pilote 3.6 ( https://github.com/fakemongo/fongo/issues/316 ).

1
int21h