Test d'intégration CRUD, est-ce correct?

Aug 27 2020

Je veux tester mon opération CRUD sur une MySqlbase de données (j'utilise dapper comme mon ORM).

Voici ce que j'ai écrit avec xUnit:

    private IPaymentRepository paymentRepository;
    private Payment testPayment;
    private string connectionString = "Database=..."; //Test DB

    public PaymentRepositoryTest()
    {
        paymentRepository = new PaymentRepository(connectionString);
        //My model has private setters so I have a builder to initialize the object.
        testPayment = new Payment.Builder()
            .Id(0)
            .Description("Test")
            .Type(1)
            .WithPercentageDiscount(10)
            .WithAdvancePercentageDiscount(0)
            .WithInstallments(new int[] { 30 }).Build;  
    }

CRÉER

[Fact]
    public void Insert_Payment_ReturnId()
    {
        long id = 0;
        using(var transaction = new TransactionScope())
        {
            id = paymentRepository.Insert(testPayment);
        }

        Assert.True(id > 0);
    }

LIS

[Fact]
    public void Get_Payment_ReturnPaymentFromDb()
    {
        using (var transaction = new TransactionScope())
        {
            long id = paymentRepository.Insert(testPayment);
            var payment = paymentRepository.GetById(id);

            Assert.NotNull(payment);
        }
    }

METTRE À JOUR

Je n'aime pas vraiment ce code, avoir un setter privé ici est malheureux car je dois recréer l'objet. Je ne suis pas non plus vraiment sûr de ne vérifier qu'un seul champ.

    public void Update_Payment()
    {
        long id = 0;

        const string UPDATED_DESCRIPTION = "Test2";

        using (var transaction = new TransactionScope())
        {
            id = paymentRepository.Insert(testPayment);

            var updatedPayment = new Payment.Builder()
            .Id(id)
            .Description(UPDATED_DESCRIPTION)
            .Type(1)
            .WithPercentageDiscount(10)
            .WithAdvancePercentageDiscount(0)
            .WithInstallments(new int[] { 30 }).Build();

            paymentRepository.Update(updatedPayment);

            updatedPayment = paymentRepository.GetById(id);

            Assert.Equal(UPDATED_DESCRIPTION, updatedPayment.Description);
        }
    }

EFFACER

Tout comme la mise à jour, je n'aime pas avoir à recréer l'objet juste pour avoir le bon ID.

[Fact]
    public void Delete_Payment_ReturnNull()
    {
        using (var transaction = new TransactionScope())
        {
            long id = paymentRepository.Insert(testPayment);

            var paymentToDelete = new Payment.Builder()
            .Id((int)id)
            .Description("Test")
            .Type(1)
            .WithPercentageDiscount(10)
            .WithAdvancePercentageDiscount(0)
            .WithInstallments(new int[] { 30 }).Build();

            paymentRepository.Delete(paymentToDelete);

            var payment = paymentRepository.GetById(id);

            Assert.Null(payment);
        }
    }

Le code fonctionne bien sûr et j'obtiens les bons résultats, mais j'ai l'impression de répéter le même test encore et encore, en particulier dans Update et Delete.

Ce code d'intégration est-il bon? Comment puis-je l'améliorer?

Réponses

5 rob.earwaker Aug 27 2020 at 04:32

L'ajout de tests d'intégration automatisés est un excellent moyen de faire passer les tests au niveau supérieur, et ce que vous avez écrit est vraiment bon pour une première tentative. Voici quelques conseils / suggestions qui pourraient vous aider tout au long de votre chemin.


Il est presque inévitable que vos tests finissent par tester efficacement la même chose plusieurs fois. Comme vous l'avez identifié, la plupart de vos tests utilisent la GetByIdméthode, et en fait tous Insertun paiement. Le seul problème potentiel que cela provoque est que si l'une de ces méthodes cesse de fonctionner, la plupart / tous vos tests échouent soudainement et il se peut que la localisation du problème ne soit pas immédiatement évidente. Dans le grand schéma des choses, ce n'est pas vraiment un problème car vous serez presque certainement en mesure de déterminer où se trouve le problème en regardant la sortie du test et / ou en parcourant le test avec un débogueur.


Bien qu'il semble que vos tests soient très similaires, beaucoup de similitudes sont dues au fait qu'ils partagent des étapes de configuration communes, par exemple l'insertion d'un paiement afin que vous ayez quelque chose à obtenir / mettre à jour / supprimer. Il est tout à fait correct d'avoir ce genre de duplication dans les tests unitaires / d'intégration, et c'est encore une fois inévitable dans la plupart des cas. Au fur et à mesure que le nombre de tests augmente, vous constaterez peut-être qu'il est judicieux d'extraire une partie de la configuration dans une méthode distincte utilisée par plusieurs tests. C'est également bien à faire, tant que cela n'affecte pas la lisibilité du test, évitez donc d'utiliser des noms de méthodes génériques comme Setup()et préférez plutôt des noms clairs (si légèrement verbeux) comme CreateRepositoryWithSinglePayment(testPayment).


Le générateur de paiement fluide rend difficile de savoir quelles propriétés sont requises et lesquelles sont facultatives (le cas échéant). Je ne peux pas vous conseiller beaucoup à ce sujet sans voir les classes Paymentou PaymentBuilder, mais si toutes les propriétés sont requises, envisagez simplement de créer un Paymentconstructeur qui attribue des valeurs de paramètre aux propriétés en lecture seule. Ensuite, il n'y a aucun doute sur les valeurs requises pour créer un fichier valide Payment.


Une chose à savoir lors des tests d'intégration est qu'il est souvent facile de se retrouver avec un état partagé entre les tests. Ici, vous insérez un paiement dans votre base de données dans le cadre de chaque test et celui-ci n'est pas toujours supprimé par la suite (ou du moins il n'y a pas de code de suppression inclus dans votre message). Étant donné que vous persistez réellement dans une base de données, il existera donc entre les tests, sauf s'il est explicitement supprimé après l'exécution de chaque test. Cela pourrait entraîner un comportement de test très déroutant à l'avenir, car vos tests pourraient ne pas tester ce que vous vouliez! Vous devez ajouter une étape de démontage en implémentant IDisposableet en supprimant chaque paiement de votre base de données de test à l'intérieur de la Dispose()méthode. Cela garantit que chaque test commencera à partir du même état frais.

Sur une note similaire, sachez que vous pouvez obtenir un comportement étrange lors de l'exécution de tests d'intégration en parallèle s'ils partagent le (s) même (s) magasin (s) de données. Tout va bien pour le moment car, par défaut, xUnit exécute des cas de test dans la même classe en série, mais si vous aviez une autre classe de test utilisant la même base de données, ceux-ci seraient exécutés en parallèle par défaut. Il existe des moyens de contrôler ce comportement, mais c'est plus quelque chose dont il faut être conscient car cela ne devrait pas être un problème étant donné votre organisation de test actuelle.


Vous ne devriez pas avoir besoin de fournir l' Paymentobjet entier pour le supprimer de la base de données. Si vous pouvez lire un paiement par son en Idutilisant la GetByIdméthode, vous devriez également pouvoir supprimer un paiement par son Id. Cela devrait simplifier Deletequelque peu le cas de test.


Il y a beaucoup d'autres choses que vous pouvez tester ici. Vous avez des tests pour les chemins «heureux», c'est-à-dire les chemins où tout se passe comme prévu, mais il y a certainement beaucoup de cas extrêmes (ou de cas inattendus) que vous pouvez également tester. Peut-être que vous avez déjà envisagé certains / tous ces éléments, mais par cœur:

  • Que se passe-t-il lorsque vous insérez un paiement qui existe déjà dans la base de données?
  • Que se passe-t-il lorsque vous essayez de lire un paiement qui n'existe pas dans la base de données?
  • Que se passe-t-il lorsque vous essayez de mettre à jour un paiement qui n'existe pas dans la base de données?
  • Pouvez-vous mettre à jour le paiement avec une nulldescription? Que se passe-t-il si la description est vraiment longue ou contient des caractères spéciaux? Vous voudrez probablement utiliser un [Theory]with [InlineData]pour énumérer plusieurs cas de test différents si vous faites cela.
  • Que se passe-t-il lors de la mise à jour de propriétés autres que la description?
  • Que se passe-t-il lorsque vous essayez de supprimer un paiement qui n'existe pas dans la base de données?