Comment tester la couche d'accès aux données?

17

J'ai une méthode DAO qui utilise Spring pour l'accès JDBC. Il calcule le taux de réussite d'un vendeur à vendre un article.

Voici le code:

public BigDecimal getSellingSuccessRate(long seller_id) {
    String sql = "SELECT SUM(IF(sold_price IS NOT NULL, 1, 0))/SUM(1) 
                  FROM transaction WHERE seller_id = ?";
    Object[] args = {seller_id};
    return getJdbcTemplate().queryForObject(sql, args, BigDecimal.class);
}

Comment dois-je procéder pour tester cette méthode ou toute méthode DAO avec JUnit? Quelles sont les meilleures pratiques pour tester la logique d'accès aux données? Je pense à le tester par rapport à une base de données embarquable chargée de certaines données, mais ne devrions-nous pas faire des tests d'intégration similaires à un environnement de production en termes de SGBDR et de schéma?

Michael
la source
Découvrez DBUnit . Il est spécialement conçu pour résoudre votre problème.
Sergio

Réponses:

15

Le problème avec l'utilisation d'une «vraie» base de données pour les tests unitaires est la configuration, la suppression et l'isolement des tests. Vous ne voulez pas avoir à créer une base de données MySQL entièrement nouvelle et à créer des tables et des données juste pour un test unitaire. Les problèmes avec cela ont à voir avec la nature externe de la base de données et votre base de données de test est en panne, vos tests unitaires échouent. Il y a également des problèmes à vérifier que vous disposez d'une base de données unique pour les tests. Ils peuvent être surmontés, mais il existe une réponse plus simple.

Se moquer de la base de données est une option, mais il ne teste pas les requêtes réelles qui sont exécutées. Il peut être utilisé comme une solution beaucoup plus simple lorsque vous voulez vous assurer que les données du DAO passent correctement par le système. Mais pour tester le DAO lui-même, vous avez besoin de quelque chose derrière le DAO pour que les données et les requêtes s'exécutent correctement.

La première chose à faire est d'utiliser une base de données en mémoire. HyperSQL est un excellent choix pour cela car il a la capacité d'émuler le dialecte d'une autre base de données - de sorte que les différences mineures entre les bases de données restent les mêmes (types de données, fonctions, etc.). hsqldb a également quelques fonctionnalités intéressantes pour les tests unitaires.

db.url=jdbc:hsqldb:file:src/test/resources/testData;shutdown=true;

Cela charge l'état de la base de données (les tables, les données initiales) à partir du testDatafichier. shutdown=truefermera automatiquement la base de données à la fermeture de la dernière connexion.

En utilisant de l'injection de dépendances , demandez aux tests unitaires de sélectionner une base de données différente de celle utilisée par les builds de production (ou test ou local).

Votre DAO utilise ensuite la base de données injectée pour laquelle vous pouvez lancer des tests sur la base de données.

Les tests unitaires ressembleront alors à quelque chose (tas de trucs ennuyeux non inclus pour plus de brièveté):

    @Before
    public void setUpDB() {
        DBConnection connection = new DBConnection();
        try {
            conn = connection.getDBConnection();
            insert = conn.prepareStatement("INSERT INTO data (txt, ts, active) VALUES (?, ?, ?)");
        } catch (SQLException e) {
            e.printStackTrace();
            fail("Error instantiating database table: " + e.getMessage());
        }
    }

    @After
    public void tearDown() {
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private void addData(String txt, Timestamp ts, boolean active) throws Exception {
        insert.setString(1, txt);
        insert.setTimestamp(2, ts);
        insert.setBoolean(3, active);
        insert.execute();
    }

    @Test
    public void testGetData() throws Exception {
        // load data
        Calendar time = Calendar.getInstance();
        long now = time.getTimeInMillis();
        long then1h = now - (60 * 60 * 1000);  // one hour ago
        long then2m = now - (60 * 1000 * 2);   // two minutes ago
        addData("active_foo", new Timestamp(then1h), true);     // active but old
        addData("inactive_bar", new Timestamp(then1h), false);  // inactive and old
        addData("active_quz", new Timestamp(then2m), true);     // active and new
        addData("inactive_baz", new Timestamp(then2m), false);  // inactive and new

        DataAccess dao = new DataAccess();
        int count = 0;
        for (Data data : dao.getData()) {
            count++;
            assertTrue(data.getTxt().startsWith("active"));
        }

        assertEquals("got back " + count + " rows instead of 1", count, 1);
    }

Et donc, vous avez un test unitaire qui appelle le DAO et utilise les données qui ont été configurées dans une base de données à la volée qui existe pour la durée du test. Vous n'avez pas à vous soucier des ressources externes ou de l'état de la base de données avant l'exécution, ou de la restauration à un état connu (enfin, «l'état connu» est «n'existe pas», ce qui est trivial pour revenir à).

DBUnit peut faire beaucoup de ce que j'ai décrit un processus plus simple dans la configuration de la base de données, la création des tables et le chargement des données. Si vous deviez utiliser la base de données réelle pour une raison quelconque, c'est de loin le meilleur outil à utiliser.

Le code ci-dessus fait partie d'un projet maven que j'ai écrit pour la preuve de concept TestingWithHsqldb sur github


la source
2
Je ne savais pas sur la partie que HSQL peut se moquer du dialecte d'un autre fournisseur de base de données. Je vous remercie.
Michael
1
@Dog, cela peut être fait via les propriétés de la base de données, telles que celles sql.syntax_mys=truequi changent le fonctionnement de hsqldb: "Cette propriété, lorsqu'elle est définie sur true, permet la prise en charge des types TEXT et AUTO_INCREMENT et permet également la compatibilité avec certains autres aspects de ce dialecte." while sql.syntax_ora=true"Cette propriété, lorsqu'elle est définie sur true, active la prise en charge des types non standard. Elle active également la syntaxe DUAL, ROWNUM, NEXTVAL et CURRVAL et permet également la compatibilité avec certains autres aspects de ce dialecte."
DBUnit est le chemin :)
Silviu Burcea
@SilviuBurcea DBUnit rend certainement beaucoup plus facile la mise en place d'un environnement de test de base de données complexe que de le faire à la main. Il est toujours parfois utile de savoir comment le faire à la main si vous en avez besoin (l'approche `` à la main '' mentionnée ci-dessus pourrait être migrée vers d'autres langues où DBUnit n'est pas une option).
Vous pouvez jeter un œil à Acolyte
cchantep
2

Tout d'abord, vous ne devez jamais effectuer de tests dans un environnement de production. Vous devriez avoir un environnement de test qui reflète votre environnement de production et y faire des tests d'intégration.

Si vous faites cela, vous pouvez faire un certain nombre de choses.

  • Écrivez des tests unitaires qui testent pour voir si le SQL approprié est soumis à un élément factice à l'aide d'un cadre de simulation tel que Mockito. Cela garantira que votre méthode fait ce qu'elle est censée faire et supprime l'intégration de l'image.
  • Écrivez des scripts SQL de test démontrant la pertinence du SQL que vous avez testé dans vos tests unitaires. Cela peut aider à résoudre tous les problèmes de réglage que vous pouvez rencontrer, car vous pouvez également exécuter des explications et autres en fonction de vos scripts de test.
  • Utilisez DBUnit, comme mentionné par @Sergio.
Matthew Flynn
la source
Woops quand j'ai dit environnement de production, je voulais vraiment en faire une simulation. Merci pour votre réponse, je vais jeter un œil à Mockito car c'est quelque chose que je voulais aussi apprendre.
Michael
1

Sur notre projet, chaque développeur exécute une base de données vide, sa structure est la même que la base de données de production.

Dans chaque test unitaire TestInitialize, nous créons une connexion et une transaction à la base de données ainsi que certains objets par défaut dont nous avons besoin pour chaque test. Et tout est annulé après la fin de chaque méthode ou classe.

De cette façon, il est possible de tester la couche sql. En fait, chaque requête ou appel de base de données doit être testé de cette manière.

L'inconvénient est qu'il est lent, nous l'avons donc mis dans un projet distinct de nos tests unitaires réguliers. Il est possible d'accélérer cela en utilisant une base de données en mémoire, mais l'idée reste la même.

Carra
la source
Si vous utilisez une base de données en mémoire, une approche drop-create avant l'exécution de toutes les suites de tests pourrait être utilisée à la place du droit de transaction de restauration, ce qui est beaucoup plus rapide.
Downhillski
Jamais pensé à le faire de cette façon auparavant. Dans nos tests, la plupart des tests créent un utilisateur "x" bien que ce soit unique. Créer une base de données une fois signifierait changer les tests pour réutiliser ces objets.
Carra
Je sais, nous sommes sur la même longueur d'onde et j'aime votre approche. votre approche garantit que chaque scénario de test peut être exécuté indépendamment, quel que soit l'ordre, et à chaque fois qu'il est exécuté, l'état de la table de données est le même.
Downhillski
C'est exact, la commande n'a alors aucune importance. Nous avons déjà vu des tests échouer parce que l'ordre d'exécution des tests unitaires est différent sur notre PC de construction et sur notre machine locale.
Carra