web-dev-qa-db-fra.com

Comment puis-je implémenter des tests unitaires dans des classes grandes et complexes?

J'implémente des tests unitaires dans un système financier comportant plusieurs calculs. L'une des méthodes reçoit un objet par paramètre avec plus de 100 propriétés et, en fonction des propriétés de cet objet, calcule le retour . Pour implémenter des tests unitaires pour cette méthode, je cet objet rempli de valeurs valides. _

Donc ... question: aujourd'hui, cet objet est peuplé via une base de données. Sur mes tests unitaires (j'utilise NUnit), je devrais éviter la base de données et créer un objet fictif, pour ne tester que le retour de la méthode. Comment puis-je tester efficacement cette méthode avec cet objet énorme? Dois-je réellement remplir manuellement les 100 propriétés? Existe-t-il un moyen d'automatiser la création de cet objet à l'aide de Moq (par exemple)?

obs: J'écris des tests unitaires pour un système déjà créé. Il n'est pas possible de réécrire toute l'architecture en ce moment .
Merci mille fois!

17
Lisa Shiphrah

Si ces 100 valeurs ne sont pas pertinentes et que vous n'avez besoin que de certaines d'entre elles, vous avez plusieurs options.

Vous pouvez créer un nouvel objet (les propriétés seront initialisées avec les valeurs par défaut, telles que null pour les chaînes et 0 pour les entiers) et n'affecter que les propriétés requises:

 var obj = new HugeObject();
 obj.Foo = 42;
 obj.Bar = "banana";

Vous pouvez également utiliser une bibliothèque comme AutoFixture qui assignera des valeurs factices à toutes les propriétés de votre objet:

 var fixture = new Fixture();
 var obj = fixture.Create<HugeObject>();

Vous pouvez affecter manuellement les propriétés requises ou utiliser le générateur de fixtures.

 var obj = fixture.Build<HugeObject>()
                  .With(o => o.Foo, 42)
                  .With(o => o.Bar, "banana")
                  .Create();

Une autre bibliothèque utile dans le même but est NBuilder


REMARQUE: Si toutes les propriétés sont pertinentes pour la fonctionnalité que vous testez et qu'elles doivent avoir des valeurs spécifiques, aucune bibliothèque ne pourra deviner les valeurs requises pour votre test. Le seul moyen est de spécifier les valeurs de test manuellement. Bien que vous puissiez éliminer beaucoup de travail si vous définissez des valeurs par défaut avant chaque test et ne modifiez que ce dont vous avez besoin pour un test particulier. C'est à dire. create helper method (s) qui créera un objet avec un ensemble de valeurs prédéfini:

 private HugeObject CreateValidInvoice()
 {
     return new HugeObject {
         Foo = 42,
         Bar = "banaba",
         //...
     };
 }

Et ensuite, dans votre test, remplacez quelques champs:

 var obj = CreateValidInvoice();
 obj.Bar = "Apple";
 // ...
13
Sergey Berezovskiy

Dans les cas où je devais obtenir une grande quantité de données correctes réelles pour les tests, j'ai sérialisé les données en JSON et les ai directement insérées dans mes classes de test. Les données originales peuvent être extraites de votre base de données, puis sérialisées. Quelque chose comme ça:

[Test]
public void MyTest()
{
    // Arrange
    var data = GetData();

    // Act
    ... test your stuff

    // Assert
    .. verify your results
}


public MyBigViewModel GetData()
{
    return JsonConvert.DeserializeObject<MyBigViewModel>(Data);
}

public const String Data = @"
{
    'SelectedOcc': [29, 26, 27, 2,  1,  28],
    'PossibleOcc': null,
    'SelectedCat': [6,  2,  5,  7,  4,  1,  3,  8],
    'PossibleCat': null,
    'ModelName': 'c',
    'ColumnsHeader': 'Header',
    'RowsHeader': 'Rows'
    // etc. etc.
}";

Cela peut ne pas être optimal lorsque vous avez beaucoup de tests comme celui-ci, car il faut beaucoup de temps pour obtenir les données dans ce format. Mais cela peut vous donner des données de base que vous pouvez modifier pour différents tests une fois que vous avez terminé la sérialisation.

Pour obtenir ce JSON, vous devez interroger séparément la base de données pour ce grand objet, le sérialiser en JSON via JsonConvert.Serialise et enregistrer cette chaîne dans votre code source - ce bit est relativement facile, mais prend un certain temps car vous devez le faire manuellement. ... Une seule fois cependant.

J'ai utilisé cette technique avec succès lorsque je devais tester le rendu des rapports et obtenir des données à partir de la base de données n'était pas une préoccupation pour le test actuel.

p.s. vous aurez besoin du paquet Newtonsoft.Json pour utiliser JsonConvert.DeserializeObject

5
trailmax

Compte tenu des restrictions (mauvaise conception du code et dette technique ... j'en ai marre), un test unitaire sera très fastidieux à peupler manuellement. Un test d'intégration hybride serait nécessaire si vous deviez utiliser une source de données réelle (et non celle en production). 

Potions potentielles

  1. Faites une copie de la base de données et remplissez uniquement les tables/données nécessaires pour renseigner la classe complexe dépendante. Espérons que le code est suffisamment modularisé pour que l'accès aux données puisse obtenir et peupler la classe complexe. 

  2. Simulez l'accès aux données et faites-le importer les données nécessaires via une autre source (fichier plat peut-être? Csv)

Tous les autres codes pourraient être centrés sur la simulation de toutes les autres dépendances nécessaires pour effectuer le test unitaire.

Sauf que la seule autre option disponible est de renseigner la classe manuellement.

D'un côté, cela a une mauvaise odeur de code, mais cela sort du cadre du PO, étant donné qu'il ne peut pas être modifié pour le moment. Je suggérerais que vous en parliez aux décideurs.

4
Nkosi

Tout d’abord, d’abord, vous devez effectuer la acquisition de cet objet via une interface si le code à cet emplacement est actuellement extrait de la base de données. Ensuite, vous pouvez vous moquer de cette interface pour retourner ce que vous voulez dans vos tests unitaires.

Si j'étais à votre place, j'extraireais la logique de calcul réelle et rédigerais des tests pour cette nouvelle classe de "calculatrice". Tout décomposer autant que vous le pouvez. Si l'entrée a 100 propriétés mais que toutes ne sont pas pertinentes pour chaque calcul, utilisez interfaces pour les séparer. Cela rendra visible l'entrée attendue, améliorant également le code.

Donc, dans votre cas, si votre classe s'appelle BigClass, vous pouvez créer une interface qui serait utilisée dans un certain calcul. De cette façon, vous ne modifiez pas la classe existante ni la façon dont l’autre code fonctionne avec elle. La logique de calcul extraite serait indépendante, testable et le code - beaucoup plus simple.

    public class BigClass : ISet1
    {
        public string Prop1 { get; set; }
        public string Prop2 { get; set; }
        public string Prop3 { get; set; }
    }

    public interface ISet1
    {
        string Prop1 { get; set; }

        string Prop2 { get; set; }
    }

    public interface ICalculator
    {
        CalculationResult Calculate(ISet1 input)
    }
1
Stefan Georgiev

Je prendrais cette approche:

1 - Ecrivez des tests unitaires pour chaque combinaison de l’objet paramètre de propriété 100, en exploitant un outil permettant de le faire pour vous (par exemple, pex, intellitest) et assurez-vous qu’ils sont tous verts. À ce stade, appelez les tests unitaires des tests d'intégration plutôt que des tests unitaires, pour des raisons qui deviendront évidentes plus tard.

2 - Refactorisez les tests en morceaux de code SOLID - les méthodes qui n'appellent pas d'autres méthodes peuvent être considérées comme véritablement testables par unité car elles ne dépendent d'aucun autre code. Les méthodes restantes ne sont encore que des tests d'intégration.

3 - Assurez-vous que TOUS les tests d'intégration fonctionnent toujours en vert.

4 - Créez de nouveaux tests unitaires pour le code nouvellement testable.

5 - Lorsque tout est vert, vous pouvez supprimer tous/certains des tests d’intégration originaux superflus - à votre gré, uniquement si vous êtes à l’aise.

6 - Lorsque tout est vert, vous pouvez commencer à réduire les 100 propriétés requises dans les tests unitaires à celles strictement nécessaires pour chaque méthode. Cela mettra probablement en évidence les zones pour une refactorisation supplémentaire, mais simplifiera de toute façon l'objet paramètres. Cela réduira à son tour les efforts des mainteneurs de code futurs, et je parierais que l'échec historique de traiter la taille de l'objet de paramètre alors qu'il comptait 50 propriétés est la raison pour laquelle il est maintenant 100. Ne pas résoudre le problème maintenant le signifiera ' ll va finir par atteindre 150 paramètres, ce qui ne laisse personne le vouloir.

1
Greg Trevellick

Utilisez une base de données en mémoire pour vos tests unitaires

Donc… ce n'est techniquement pas une réponse, comme vous l'avez dit, les tests unitaires, et utiliser une base de données en mémoire rend les tests d'intégration, et non les tests unitaires. Cependant, je constate que parfois, face à des contraintes impossibles, il faut donner quelque chose et cela peut être une de ces occasions.

Ma suggestion est d'utiliser SQLite (ou similaire) dans vos tests unitaires. Il existe des outils pour extraire et dupliquer votre base de données réelle dans une base de données SQLite. Vous pouvez ensuite générer les scripts et les charger dans une version en mémoire de la base de données. Vous pouvez utiliser l'injection de dépendance et le modèle de référentiel pour définir le fournisseur de base de données différent dans vos tests "unitaires" par rapport au code réel. 

De cette façon, vous pouvez utiliser vos données existantes, les modifier quand vous en avez besoin comme conditions préalables à vos tests. Vous devrez reconnaître que ce n'est pas un vrai test unitaire ... ce qui signifie que vous êtes limité à ce que la base de données peut réellement générer (les contraintes de table empêchent de tester certains scénarios). Vous ne pouvez donc pas effectuer de test unitaire complet dans ce sens. De plus, ces tests s'exécutent plus lentement car ils fonctionnent réellement avec une base de données. Vous devrez donc prévoir le temps supplémentaire nécessaire à l'exécution de ces tests. (Bien qu’ils restent généralement assez rapides.) Notez que vous pouvez vous moquer de toutes les autres entités (par exemple, s’il existe un appel de service en plus de la base de données, c’est toujours un potentiel factice).

Si cette approche vous semble utile, voici quelques liens pour vous aider à démarrer.

Convertisseur SQL Server vers SQLite:

https://www.codeproject.com/Articles/26932/Convert-SQL-Server-DB-to-SQLite-DB

SQLite studio: https://sqlitestudio.pl/index.rvt

(Utilisez-le pour générer vos scripts pour une utilisation en mémoire)

Pour utiliser en mémoire, procédez comme suit:

TestConnection = new SQLiteConnection ("FullUri = file :: memory:? Cache = shared");

J'ai un script distinct pour la structure de base de données du chargement de données, mais, c'est une préférence personnelle.

En espérant que ça aide et bonne chance.

0
Reginald Blue