2
votes

Test unitaire avec des types génériques

Je veux tester un test unitaire avec des génériques, et je me démène pour trouver le bon moyen.

J'ai ceci

  var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;

L'erreur est dans cette ligne

[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
public void ReadFromCsvFileWithConfigurationMapTest<T,Tmap>(T t, Tmap tmap, int totalRowsExptected)
{
   //Arrange

   //Act
    var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;

     var result = new List<object>(records);

     //Assert
     result.Should().NotBeNullOrEmpty();
     result.Should().HaveCount(totalRowsExptected);
}

Dire que T et Tmap doivent être un type de référence.


0 commentaires

4 Réponses :


1
votes

Pensez à utiliser la réflexion pour appeler le sujet générique testé

 var records = genericMethod.Invoke(csvService, new object[] { _csvToRead, "," }) as IEnumerable<object>;

En utilisant le csvService , obtenez le type via GetType ()

var genericMethod = method.MakeGenericMethod(t, tmap);

afin d'avoir accès à ses informations sur les membres.

Recherchez le membre que vous souhaitez appeler par son nom

var method = serviceType.GetMethod("ReadFileCsv");


2 commentaires

Whoa Nikosi, super, ça marche. J'aime votre explication, facile à comprendre. 5 début. Je viens de changer le paramètre Invoke. Le deuxième paramètre son paramètre object [], donc je crée ceci. var records = genericMethod.Invoke (csvService, nouvel objet [] {_csvToRead, ","}) comme IEnumerable ;


Votre réponse résout le problème, mais pouvez-vous supprimer ou modifier la première ligne? J'imagine que cette réponse est citée comme une démonstration qu'il n'y a pas de méthodes de test génériques dans NUnit. Il y en a certainement, mais je ne suis pas sûr que ce soit la meilleure solution ici ... Cependant, pour le plaisir, je vais voir si je peux en écrire une ensuite.



2
votes

Alternativement, si vous ne prévoyez pas d'utiliser plusieurs attributs TestCase sur votre test avec différents types, alors il n'est pas nécessaire de donner au test des paramètres génériques. Vous pouvez simplement passer explicitement les types dans les paramètres de type:

[Test]
public void ReadFromCsvFileWithConfigurationMapTest() => ReadFromCsvFile<CalendarGeneralCsv, CalendarGeneralCsvMap>(121);

[Test]
public void ReadFromCsvFileWithOtherMapTest() => ReadFromCsvFile<CalendarGeneralCsv, OtherGeneralCsvMap>(151);

private void ReadFromCsvFile<T, TMap>(int expectedValue)
{
    //Arrange

    //Act
    var records = csvService.ReadFileCsv<T, TMap>(_csvToRead, ",") as IEnumerable<object>;

    var result = new List<object>(records);

    //Assert
    result.Should().NotBeNullOrEmpty();
    result.Should().HaveCount(expectedValue);
}

Si vous souhaitez cependant exécuter le même scénario de test pour plusieurs types / valeurs, vous pouvez extraire la logique de test réelle dans un générique méthode. Ensuite, vous pouvez créer un nouveau test pour chaque ensemble de données que vous souhaitez tester, en passant explicitement les types dans la méthode générique:

public void ReadFromCsvFileWithConfigurationMapTest()
{
    //Arrange

    //Act
    var records = csvService.ReadFileCsv<CalendarGeneralCsv, CalendarGeneralCsvMap>(_csvToRead, ",") as IEnumerable<object>;

    var result = new List<object>(records);

    //Assert
    result.Should().NotBeNullOrEmpty();
    result.Should().HaveCount(121);
}


2 commentaires

Bonjour devNull, la solution que vous avez donnée, je l'ai déjà implémentée, mais je dois tester plusieurs types génériques. Merci quand même pour votre réponse.


@jolynice J'ai mis à jour ma réponse avec une autre option qui vous permet d'écrire plusieurs tests pour le même scénario de test, en passant différents types / valeurs pour chaque test.



2
votes

Bien qu'il y ait déjà une réponse acceptée (NOTE: elle a changé depuis que j'ai initialement publié) , j'aimerais proposer une autre façon d'utiliser la réflexion. Divisez la méthode de test en deux méthodes, une méthode de tremplin et une méthode de test générique . Il y a plusieurs avantages:

  • La méthode de test générique ressemble plus à n'importe quelle autre méthode de test. Il n'y a pas de réflexion indépendante mélangée.

  • La méthode de test générique peut être exécutée normalement, également parce qu'elle n'a pas de réflexion indépendante mélangée.

  • Les modifications apportées au composant testé sont plus susceptibles de déclencher une erreur du compilateur dans le projet de test, vous savez donc que la méthode de test générique, et probablement la méthode springboard, doivent être mises à jour. De plus, en raison de l'endroit où l'exception est levée, il est plus clair au moment de l'exécution que c'est à cause de la réflexion de soutien et non de la façon dont le composant est utilisé.

  • La méthode springboard n'a pas besoin de savoir quoi que ce soit sur le composant testé, seulement comment appeler la méthode de test générique.

  • Le modèle peut être reproduit facilement et de manière cohérente car il y a très peu de variation.

Voici un exemple basé sur la question et la réponse acceptée:

[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
[TestCase(typeof(CalendarCustomCsv), typeof(CalendarCustomCsvMap), 80)]
public void ReadFromCsvFileWithConfigurationMapTest(Type t, Type tmap, int totalRowsExpected)
{
    GetType().GetMethod(nameof(GenericReadFromCsvFileWithConfigurationMapTest))
        .MakeGenericMethod(t, tmap)                         // <-- Type parameters go here
        .Invoke(this, new object[] { totalRowsExpected });  // <-- inputs go here
}

public void GenericReadFromCsvFileWithConfigurationMapTest<T, Tmap>(int totalRowsExpected)
    where T : class
    where Tmap : class
{
    // Arrange

    // Act
    var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;

    // Assert
    records.Should().NotBeNull();

    var result = new List<object>(records);

    result.Should().NotBeNullOrEmpty();
    result.Should().HaveCount(totalRowsExptected);
}

Points d'intérêt

Il utilise GetType () car il recherche une méthode du même type (la classe de test). Cela réduit les variations afin que le modèle puisse être reproduit plus facilement.

La méthode de test générique a un nom différent (peu importe ce que c'est tant qu'il est différent) de sorte que GetMethod n'a pas besoin de spécifier les types de paramètres. Il ne devrait y avoir qu'une seule méthode portant ce nom et elle est publique donc elle n'a pas besoin non plus de BindingFlags . Vous pouvez également le rendre privé, ajoutez simplement BindingFlags.NonPublic | BindingFlags.Instance . Remarque: toutes les versions de framework n'ont pas une surcharge prenant BindingFlags . Vous devrez trouver une alternative si vous voulez la rendre privée.

La méthode de test générique doit inclure les contraintes. Cela fait des contraintes une partie formelle de votre test. La réflexion échouera au moment de l'exécution de toute façon si les contraintes ne sont pas respectées, mais en les utilisant sur la méthode de test générique, vous êtes susceptible d'écrire un meilleur test dès le début. Vous avez mentionné que T et Tmap doivent être des types de référence, donc ceux-ci sont inclus ci-dessus.

Enfin, votre tremplin est capable de définir plusieurs cas de test, comme vous avez indiqué que vous devez être en mesure de le faire, j'ai donc inclus un autre calendrier et une autre cartographie ci-dessus.


1 commentaires

Bonjour madreflection, devNull, DavidG, Charlie et Nikosi, Merci à tous pour toute la solution que vous avez donnée. J'apprécie beaucoup cette communauté.



2
votes

Je ne répondrais généralement pas là où il y a déjà plusieurs réponses et une est acceptée, mais elles semblent toutes être basées sur l'hypothèse que les méthodes de test ne peuvent pas être génériques. Ils peuvent certainement. Ma mémoire me dit que c'était autrefois bien documenté, mais cela ne semble plus être - ou ma mémoire est fausse - ce qui explique pourquoi vous ne pensez peut-être pas que c'est possible.

Il est possible qu'une solution générique ne soit pas la meilleure ici, mais cela semble être une chose amusante à essayer et peut être meilleure ou clarifier pourquoi la solution acceptée est meilleure. Je ne peux aller aussi loin avec les informations déjà fournies, mais si Jolynice collabore, peut-être que nous pourrons apprendre quelque chose. :-)

Alors ... voici un premier coup d'œil à une solution, que je modifierai si plus d'informations me reviennent.

La solution d'origine dans la question provoque une erreur car les contraintes de la méthode générique ReadFileCsv (...) ne sont pas satisfaites. Nous ne savons pas ce qu'ils sont, mais de l'erreur ils incluent T: class et Tmap: class . Donc, la première étape pour une réponse correcte est de reproduire toutes les contraintes de la méthode appelée sur la méthode de test elle-même.

MISE À JOUR: Ce code ne fonctionne pas réellement. Petite histoire, j'ai la fonctionnalité localement et je pensais qu'elle avait été ajoutée à NUnit mais ce n'est pas le cas. Voir également le texte UPDATE ci-dessous ...

[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
public void ReadFromCsvFileWithConfigurationMapTest<T,Tmap>(int totalRowsExptected)
    where T : class
    where Tmap : class
{
   //Arrange
    
   //Act
    var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
    
     var result = new List<object>(records);
    
     //Assert
     result.Should().NotBeNullOrEmpty();
     result.Should().HaveCount(totalRowsExptected);
}

UPDATE en réponse au commentaire @Jannes

Vous pouvez créer une méthode générique sans paramètres en C #. Si vous avez utilisé une telle méthode comme méthode de test, NUnit aurait besoin de connaître les types réels à utiliser pour l'appeler. Malheureusement, ce n'est pas possible.

Actuellement, NUnit ne peut déduire les types réels qu'à partir des arguments que vous fournissez. Cela signifie qu'il doit y avoir au moins un argument pour chaque paramètre Type de la méthode générique.

C'est clairement une lacune dans NUnit et cela a été discuté dans divers numéros sur GitHub. Jusqu'à présent, aucune proposition n'a été acceptée. Consultez les numéros 150, 1215, 2562 et 3576 sur https://github.com/nunit/nunit/issues par exemple.


10 commentaires

Etes-vous sûr que c'est correct? Je n'utilise pas NUnit, mais dans XUnit, les paramètres de l'équivalent de l'attribut TestCase seraient des instances des types génériques utilisés. Donc quelque chose comme ceci: [TestCase (new CalendarGeneralCsv (), new CalendarGeneralCsvMap (), 121)]


Bonjour Charlie, Merci de participer à ce problème. ;), J'essaye votre solution et j'ai toujours la référence. Erreur CS0311 Le type 'Tmap' ne peut pas être utilisé comme paramètre de type 'TMap' dans le type ou la méthode générique 'CsvServices.ReadFileCsv (string, string)'. Il n'y a pas de conversion de référence implicite de 'Tmap' à 'CsvHelper.Configuration.ClassMap'


jolynice: mise à jour de l'exemple de code. Si vous voyez d'autres erreurs, veuillez poster la déclaration (c'est-à-dire l'en-tête) de la méthode ReadFileCsv .


Peut-être que je fais quelque chose de mal, mais cela ne semble pas fonctionner (plus?). Il en résulte simplement "Arguments fournis pour la méthode sans paramètres". Votre réponse est littéralement le seul endroit sur Internet où cette construction est même mentionnée.


@Jannes Si vous recevez ce message, votre méthode de test ne prend pas de paramètres. Il n'est actuellement pas possible d'avoir une méthode de test générique sans paramètres dans NUnit. Voir ma note mise à jour sur la question pour plus de détails.


ok, alors maintenant vous dites ce que le reste d'Internet disait aussi: nunit ne peut déduire le type qu'à partir des valeurs réelles des arguments. Il n'y a aucun moyen de passer des typeparams s'ils ne sont pas utilisés dans les arguments. Je suppose que j'ai été induit en erreur par votre réponse avec l'exemple de code où vous faites clairement exactement cela. Ainsi que le commentaire qui dit "Mais si les arguments initiaux fournis sont des types et qu'il n'y a pas de paramètre correspondant, ils sont alors considérés comme des arguments de type."


@Jannes Oui c'est vrai. J'ai écrit du code pour gérer les arguments de type initial, ce que je pensais être libéré, mais apparemment pas. C'était mon impression lorsque j'ai répondu à ce problème au début de 2019. Pour autant que je sache, cette mauvaise réponse était restée environ 11 heures avant de la modifier. Ai-je oublié quelque chose? Je ne trouve nulle part le commentaire que vous avez cité.


Ok merci d'avoir fait la promotion de cette fonctionnalité et même de l'avoir écrite. En plus de répondre aux questions ici. Très malheureux qu'il ne soit jamais arrivé. Cela me semble très utile. Il vaut peut-être mieux mettre un avertissement en haut de cette réponse indiquant que cela ne fonctionne pas (encore). Pourrait sauver les futurs gens un peu de confusion. L'exemple de code dans la réponse montre toujours la solution qui ne fonctionne pas. Merci également d'avoir créé TestFixtureSource avec un support de type générique. J'ai aussi utilisé ça. BTW: Je faisais référence au troisième commentaire de cette réponse (commence par "DavidG: Normalement ...").


@Jannes Ah ... J'ai édité le texte mais pas l'exemple. Je ferai ce que vous suggérez. Le commentaire à Davidg est faux mais je l'ai laissé là parce que c'est de l'histoire ... c'est-à-dire que j'ai vraiment dit cette mauvaise chose, et je l'ai corrigé plus tard dans les commentaires suivants. Mais peut-être avez-vous raison de dire que cela pourrait dérouter les gens, alors je vais le supprimer. Malheureusement, la plate-forme ne permet pas d'éditer des commentaires.


Votre point sur l'histoire a certainement du mérite, même si je pense que ce n'est pas la priorité sur ce site Web. C'est obtenir de bonnes questions et de bonnes réponses sans avoir à faire défiler des pages interminables de discussion comme c'est le cas sur des forums plus classiques. Même cette discussion sort du sujet, je présume. Merci encore!