Nunit: Fonctionnalité d'instance par cas de test

Créé le 24 nov. 2017  ·  112Commentaires  ·  Source: nunit/nunit

Le modèle mental de tout le monde semble être qu'il existe une instance créée par cas de test lors de la parallélisation au sein d'un appareil (https://github.com/nunit/nunit/issues/2105, https://github.com/nunit/nunit/issues /2252, https://github.com/nunit/nuni/issues/2573) même si cela n'a jamais été le cas. J'ai tendance à résoudre ce problème en utilisant toujours des classes statiques et en définissant une classe de luminaires imbriqués jetables qui offre une expérience plus riche à certains égards ( exemple ), mais ce n'est peut-être pas ce que nous voulons pour tout le monde. Le léger inconvénient ici est dans la terminologie, que la classe statique est ce que NUnit considère comme le luminaire, mais le vrai luminaire est la classe imbriquée.

Ce n'est pas une option de faire de l'instance par cas de test la valeur par défaut car cela casse les appareils non parallèles qui dépendent d'un test pouvant accéder à l'état à partir d'un autre test. Les personnes utilisant l'attribut [Order] sont probablement les seules personnes que cela affecterait, mais cela briserait totalement tout test dépendant de l'ordre auquel je peux penser.

J'aime la proposition de Charlie :

  • Un attribut de niveau d'assemblage qui permet de définir la façon dont les appareils de test sont créés pour être une instance unique ou une instance par appareil.
  • L'instance unique fonctionnerait comme maintenant
  • L'instance par appareil créerait un nouvel appareil pour chaque test
  • SOIT effectuer des activités de configuration et de démontage uniques pour chaque test (c'est-à-dire plus "une fois") OU faire en sorte que OneTimeSetUp et OneTimeTearDown soient signalés comme des erreurs.

Extensions possibles :

  • Faites en sorte que cette classe soit également au niveau, en permettant aux luminaires individuels d'avoir des cycles de vie différents.
  • Disposez d'un troisième paramètre au niveau de l'assemblage qui exécute automatiquement des instances distinctes pour tout appareil contenant un ou plusieurs tests exécutés en parallèle. (difficile de toujours savoir à l'avance)

Cependant, je dirais de commencer par les bases.

À l'heure actuelle, il est garanti qu'il s'agit d'un bogue s'il existe un état d'instance mutable et qu'un appareil s'exécute en parallèle avec lui-même, nous pourrions donc avoir une option Auto qui passe à l'instance par cas de test si les cas de test de l'appareil sont configurés pour s'exécuter en parallèle et éviter tout changement de rupture . Je pense que Auto a le potentiel d'être déroutant, mais cela pourrait aussi être la valeur par défaut la plus sensée afin que tout le monde n'ajoute pas [assembly: InstancePerTestCase] par routine.

Je suis en faveur des garde-corps. OneTimeSetUp et OneTimeTearDown doivent être signalés comme des erreurs si le projecteur effectue une instance par scénario de test. Peut-être à moins qu'ils ne soient statiques ?

done feature normal docs releasenotes

Commentaire le plus utile

une chance que nous puissions faire réviser cela? j'ai l'impression que nous nous rapprochons vraiment

Tous les 112 commentaires

Actuellement, vous __pouvez__ partager l'état entre les tests parallèles à condition (1) de définir l'état dans le constructeur ou la configuration unique et (2) de ne le modifier dans aucun test. Vous pouvez même vous en tirer dans certains cas si l'état est défini sur une constante dans le test ou la configuration, à condition de ne jamais le définir sur une autre valeur. Ce dernier est dégoûtant, même si je pense que nous avons des tests NUnit qui le font.

Ma pensée est que nous ne pouvons pas changer la valeur par défaut de sitôt, voire jamais. Avoir un état mutable ne garantit actuellement pas un bogue. Ce n'est un bug que si vous le faites muter et nous ne le savons pas à moins que nous ne procédions à une analyse de code - ce que nous ne faisons pas !

Si vous ne modifiez aucun état d'instance au cours d'un test, d'une configuration ou d'un démontage, vous ne serez pas brisé. Si vous le faites, vous êtes déjà en panne tant que vos tests peuvent s'exécuter en parallèle avec eux-mêmes. Il n'y a aucun moyen que tu ne sois pas brisé auquel je puisse penser. C'est la justification de l'option Auto, si nous pensons que la complexité résultante pour l'utilisateur vaut le résultat des "valeurs par défaut sensibles".

Vous avez raison, mais je continuerais à soutenir qu'il s'agit d'une extension potentielle, dont nous n'avons pas besoin de décider maintenant. Faisons cela de manière itérative plutôt que d'essayer de prendre toutes les décisions maintenant.

je suis avec @CharliePoole, les valeurs par défaut doivent rester telles

peut-être un attribut de niveau de fixation pour déclencher une instance par thread plutôt qu'une instance partagée pour tous les threads.

@BlythMeister Ne vous inquiétez pas ! Nous n'envisageons aucun changement qui entraînerait la rupture du code existant.

C'est intéressant. Je pense que l'instance par cas de test est plus facile à comprendre que l'instance par thread. La manière dont les tests sont affectés aux threads ne serait pas fiable, donc je ne peux penser à aucun avantage qui aurait par rapport à l'instance par cas de test.

ouais, j'utilise le terme thread pour signifier une exécution parallèle... ce qui est factuellement inexact.
mes excuses.

En termes d'XP (et j'espère que tout le monde sait que c'est toujours mon point de vue), nous avons trois histoires, qui se construisent l'une sur l'autre...

  1. L'utilisateur peut spécifier le comportement de l'instance par cas de test globalement pour tous les appareils d'un assemblage.

  2. L'utilisateur peut spécifier le comportement d'instance par cas de test sur un appareil.

  3. Les utilisateurs peuvent spécifier un comportement d'instance par cas de test pour les appareils contenant des tests pouvant s'exécuter en parallèle.

J'aimerais voir (1) complètement mis en œuvre dans ce numéro. Je pense que nous devrions juste retirer (2) de nos têtes - ou du moins de la partie qui écrit le code - pour le moment. Je pense que (3) est une possibilité mais présente des pièges que nous avons déjà rencontrés dans d'autres problèmes. (Un appareil ne sait pas s'il va exécuter des cas de test en parallèle !) Parlons de celui-là JustInTime, qui serait après que (1) et (2) soient terminés et fusionnés. FWIW, je pense que nous avons tendance à nous nouer en essayant de penser plusieurs étapes à l'avance. J'ai mentionné (2) et (3) pour que nous ne les bloquions en aucune façon, ce qui semble assez facile à éviter.

Je pense que nous sommes tous d'accord alors. J'ai cependant quelques priorités NUnit avant cela, donc je vais laisser les autres commenter et éventuellement reprendre.

La beauté de faire cela avec un attribut plutôt qu'un paramètre de ligne de commande est que cela fonctionnera pour n'importe quel coureur.

Je suis d'accord avec @CharliePoole que cela devrait être un attribut. Les tests devront être conçus en gardant à l'esprit l'instance par test ou l'instance par appareil, ils doivent donc toujours fonctionner de cette façon. Je préfère également ne faire que l'option 1. Je pense que si vous voulez ce style de test, vous devriez l'accepter pour l'ensemble de votre suite de tests. Vous permettre de vous inscrire à des niveaux inférieurs introduit une complexité qui, à mon avis, n'a pas un bon retour sur investissement.

Juste curieux de savoir s'il existe des plans imminents pour implémenter cette fonctionnalité ?

@rprouse Ceci est toujours marqué comme nécessitant une conception, mais il semble que la discussion ait déjà tout couvert.

@CharliePoole a convenu que cela peut passer de la conception. @pfluegs, nous n'avons pas commencé à travailler dessus au-delà de sa conception, il n'y a donc pas encore de plans.

Pas sûr que quelqu'un y prête autant d'attention, mais l'absence de discussion ou d'étiquettes de conception implique que quelqu'un pourrait se porter volontaire. ??

Je viens d'être gravement mordu par ce problème même - +1 pour le correctif.

Au cas où nous voudrions plus d'options, voudrions-nous :

```c#
[assemblage : FixtureCreation(FixtureCreationOptions.PerTestCase)]

And:
```diff
namespace NUnit.Framework
{
+   public enum FixtureCreationOptions
+   {
+       Singleton, // = default
+       PerTestCase
+   }
}

Ma propre réflexion à ce sujet a un peu changé, dans le contexte d'un nouveau cadre au moins.

Je pense maintenant qu'il devrait y avoir deux attributs au niveau de la classe, un pour chaque comportement. Je ferais fonctionner TestFixture avec une instance par test et Shared Fixture fonctionnerait comme NUnit le fait maintenant. Évidemment, c'est un changement décisif dans le contexte de NUnit, mais l'approche générale pourrait valoir la peine d'être envisagée.

@CharliePoole [TestFixture(FixtureOptions.PerTestCase)] semble être une bonne syntaxe, mais je souhaite également vivement un attribut au niveau de l'assemblage afin que je puisse adhérer à chaque projet de test. À quoi voudrions-nous qu'un attribut au niveau de l'assemblage ressemble ?

D'accord, c'est pourquoi j'ai exprimé la réserve WRT de ce que je ferais sur un nouveau framework. Dans le cas de NUnit, cependant, vous ne voudriez pas changer la sémantique de TestFixture et je ne peux pas penser à un très bon nom pour le comportement d'instance par cas. Si vous aviez deux noms, tout ce que vous auriez à faire pour obtenir le comportement souhaité sur tous les appareils serait de faire une édition globale de votre fichier source.

Je n'aime pas les paramètres globaux au niveau de l'assemblage car ils finissent par dérouter certains développeurs, qui peuvent ne pas être au courant de ce qui est défini dans le fichier AssemblyInfo. Actuellement, nous en autorisons quelques-uns, donc je suppose qu'il n'y a pas grand mal à en ajouter un autre. Je n'ai aucune idée du nom de l'attribut de niveau d'assemblage, sauf qu'il doit indiquer que nous définissons une option (par défaut ?) Pour tous les appareils de l'assemblage.

Je n'aime pas les valeurs par défaut au niveau de l'assemblage dans une certaine mesure également, pour la raison même que vous mentionnez. Par rapport à l'ajout d'un nouvel attribut à chacune de mes classes de luminaires actuellement sans attribut, l'attribut de niveau d'assemblage est l'itinéraire que je préférerais.

@nunit/framework-team Qu'en pensez-vous ? Pour l'introduction initiale de la fonctionnalité :

  • oui/non au réglage au niveau de l'assemblage ?
  • oui/non au réglage au niveau de l'appareil ?
  • oui/non au réglage au niveau de la méthode ?
  • oui/non au réglage au niveau du cas de test ?

Bon point sur les classes de luminaires sans attribut.

Je dirais oui, oui, non, non

Je suis d'accord avec Charlie - avec la précision que "niveau de l'appareil" signifie "niveau de la classe". (Par opposition à, par exemple, une méthode source de cas de test. 😊)

Qui aide! Donc, le problème avec [TestFixture(FixtureCreationOptions.PerTestCase)] est que plusieurs attributs TestFixture sont autorisés sur la même classe et ils pourraient alors se contredire. Mieux et plus simple peut-être d'avoir un nouvel attribut nommé applicable aux assemblys et aux classes ?

```c#
[assembly : ShareFixtureInstances(false)]

[ShareFixtureInstances(false)]
classe publique SomeFixture
{
// ...
}

Framework API:

```diff
namespace NUnit.Framework
{
+   [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)]
+   public sealed class ShareFixtureInstancesAttribute : PropertyAttribute
+   {
+       public bool ShareFixtureInstances { get; }

+       public ShareFixtureInstancesAttribute(bool shareFixtureInstances);
+   }
}

Ça a l'air bien. Quel est le comportement d'héritage de ceci - si j'ai l'attribut sur une classe de base, avec ses propres tests, puis hérite également de cette classe ? ??

Vraisemblablement, comme il s'agit d'un attribut "classe", il doit être sur la classe réelle à partir de laquelle les tests en question sont exécutés ?

De plus, je ne suis pas totalement fan du nom - comme un appareil n'est pas toujours une classe en termes de nunit, je ne pense pas (par exemple, tous les tests dans un testcasesource - à moins que 'suite' et 'fixture' aient une sémantique plus forte sens que je ne le pense, en termes Nunit !)

Que diriez-vous de quelque chose comme InstancePerTest... mais mieux nommé...

D'accord sur le niveau d'assemblage et de classe mais pas plus profond. Je préfère aussi quelque chose dans le sens de la suggestion de ClassInstancePerTest(bool) . Je suppose également que l'attribut au niveau de la classe remplacerait le niveau de l'assemblage et je m'attendrais à ce qu'il soit hérité. Je pense que l'héritage est important car vous voudriez généralement une instance par test basée sur les membres d'une classe combinée au parallélisme. Vous voudriez donc que cela soit automatiquement appliqué aux classes dérivées.

J'ai été une fois vraiment surpris que NUnit ne crée pas d'instance par test lorsqu'un type d'appareil de test est constructible. J'ai toujours pensé aux tests unitaires comme, je ne sais pas, test unitaire : vous configurez l'échafaudage, exécutez le test, et l'échafaudage est parti. Pas de transfert d'état par inadvertance entre les tests.

Ayant appris cela. J'ai, comme @jnm2 , instancie les tests manuellement. Mais lorsque les tests, par exemple, créent et suppriment des fichiers, les gestionnaires de cas deviennent vraiment lourds : ils doivent être IDisposable ; Plus de [TearDown] magique pour toi. :( Pas de moyen cohérent de distinguer les erreurs de test des erreurs d'échafaudage non plus. C'est un cas d'utilisation vraiment douloureux.

Si je peux intervenir sur le design, je suis avec @CharliePoole sur oui, oui, non, non.

Le nom PerTestInstance , InstancePerTest ou quelque chose du genre sonne bien mieux que ShareFixtureInstances . Je n'aime pas du tout le mot Class dans le nom de l'attribut, car lorsque je teste du code F#, je ne pense pas que les instances de type soient des classes . Mais c'est sûr que je peux vivre avec ça ! :)

Oh mon Dieu, j'ai hâte de voir cette fonctionnalité !

@kkm000 Je suis d'accord sur l'utilisation de "class". NUnit a généralement utilisé des noms distincts de l'implémentation. Même alors, les gens font des suppositions, mais il vaut mieux être cohérent en évitant de telles choses.

Exemples de ce dont je parle : Test, TestFixture, SetUpFixture, Worker (pas de thread), etc. En principe, une classe __pourrait__ représenter un seul test, pas un appareil... Je ne dis pas que nous faisons ça ou même devrait, mais __nous pourrions__.

@kkm000

Merci pour le commentaire! Vous n'intervenez pas du tout. Notre objectif de conception est d'être indépendant de la langue dans la mesure du possible.
Outre l'accent mis sur C#, une autre chose à propos du mot class : et si un jour nous supportions des fixtures qui ne sont pas des classes ?

[InstancePerTestCase] contournerait le problème, mais cela pourrait perdre en clarté intuitive.

Aujourd'hui, les appareils et les classes ne divergent pas beaucoup, mais s'ils ont divergé, je suis à peu près sûr que cet attribut concerne les appareils plutôt que les classes.

Les suites sont un concept organisationnel générique, tandis que les fixtures sont des choses qui contiennent la logique de configuration/démontage et l'état de test et sont transmises aux méthodes de test (actuellement en tant qu'argument this méthodes d'instance). Les luminaires ne doivent pas nécessairement faire partie de la hiérarchie organisationnelle. Imaginons des appareils divergeant du concept de classes et divergeant du concept de suites de tests :

```c#
classe statique publique SomeTestSuite // Pas un appareil : pas de logique de configuration/démontage et pas d'état de test
{
[Test] // Test unique, pas une suite paramétrée ;
// l'appareil est injecté via un paramètre au lieu de via l'implicite this paramètre
vide statique public SomeTest (appareil SomeFixture)
{
fixture.SomeService.SomeProperty = true;
luminaire.UneAction();
fixture.AssertSomething();
}
}

[Test Fixture]
structure publique SomeFixture : IDisposable
{
public SomeService SomeService { get; } // État

public SomeFixture() // This was almost legal syntax in C# 6, but I'm making a point 😁
{
    // Setup
}

public void Dispose()
{
    // Teardown
}

public void SomeAction()
{
    // Helper method
}

public void AssertSomething()
{
    // Helper method
}

}
```

Dans l'exemple ci-dessus, [FixtureInstancePerTestCase] ou [SharedFixtureInstances(false)] n'est pas axé sur les classes et n'est pas axé sur la hiérarchie (suites) : il est axé sur les appareils, et les appareils sont la logique et l'état de configuration/démontage réutilisables. Plus précisément, il s'agit de savoir si un nouveau SomeFixture est créé pour chaque cas de test ou si le même est passé à chaque test.

Étant donné que cet attribut contrôlera si différents ITests partageront des instances ITest.Fixture, et la chose contrôlée est tout ce que nous décorons avec [TestFixture] (un autre attribut avec Fixture dans le nom), cela ressemble à un option _cohérent_ pour utiliser le mot Fixture.

(J'apporte des points qui semblent importants à discuter, mais je ne suis pas très investi dans ce que le nom finit par être.)

@jnm2 Vous avez tout à fait raison de dire que le concept d'une suite et d'un luminaire sont séparables. NUnit, cependant, a suivi l'exemple de tous les frameworks de test xunit existants au moment de sa création en combinant les deux. Il s'écartait des autres cadres en rendant le luminaire partagé.

À ma connaissance, xunit.net a été le premier (seul ?) framework à faire du luminaire une entité distincte de la hiérarchie d'héritage de test.

Lorsque j'ai créé des tests paramétrés, j'aurais pu introduire TestCaseSource en tant que "fixation" distincte. Je ne l'ai pas fait et je pense que les changements nécessaires pour le refaire seraient brisés maintenant.

Je pense que la raison pour laquelle les gens poseraient parfois des questions (avant le parallélisme) sur le comportement instance par cas est que c'est ce que font les autres frameworks. Pour une raison similaire, ils __n'ont pas__ demandé de luminaires séparables.

Bref, voici ce que j'en pense...

  1. Nous avons besoin d'un comportement d'instance par cas de test.

  2. Il ne peut pas s'agir de la valeur par défaut en raison de la compatibilité descendante.

  3. Un luminaire séparable est une bonne idée, mais c'est aussi une question distincte.

  4. Les appareils séparables semblent au moins aussi complexes à mettre en œuvre que les appareils d'instance. Bien sûr, vous n'auriez pas à vous soucier de la rétrocompatibilité dans ce cas, ce qui facilite les choses.

Juste pour confirmer les points 3 et 4, j'ai conçu mon échantillon de code non pas comme une suggestion de nouvelles fonctionnalités, mais comme une expérience de pensée destinée à nous aider à trouver le bon nom pour cet attribut. L'expérience de pensée me dit que nous sommes vraiment concentrés sur l'idée d'un luminaire, pas tellement sur une classe ou une suite, même s'ils coïncident en ce moment.

Quelle que soit la forme qu'ils prennent, les appareils sont des définitions réutilisables de configuration/démontage/état. Je crois que c'est le sens du mot. Les instances de luminaire (occurrences de configuration/démontage/état) sont ce que notre nouvel attribut décide de partager ou de ne pas partager entre les cas de test. Je pense que FixtureInstances quelque part dans le nom apporterait une clarté maximale.

Cela semble-t-il juste pour tout le monde ? Si non, quels autres points pouvons-nous considérer?

Ah ! Désolé, je pensais que vous aviez touché un très bon point, généralement négligé, mais j'avais aussi peur que nous fassions une digression. :slightly_frowning_face:

Je pense que le nom complet (quel que soit votre type) devrait inclure à la fois Fixture et Instance. La façon dont cela est fait dépend de si vous parlez d'un nom d'attribut ou d'une énumération utilisée dans sa construction. Je ne sais pas si vous avez choisi l'un ou l'autre.

Oui, bonne remarque.

Si nous faisons une énumération, la saisie est plus longue. Cela est plus important pour les attributs ciblés par classe que pour ceux ciblés par l'assemblage. Nous devons également éviter d'ajouter une énumération en tant que paramètre à TestFixtureAttribute car plusieurs TestFixtureAttributes sont autorisés sur la même classe et le paramètre de l'un peut contredire le paramètre de l'autre. Nous pouvons gérer cela via une erreur au moment du test, mais il est généralement agréable de rendre les états invalides non représentables.

Voyons quelques options et nous pouvons modifier à partir de là.
Nous recherchons quelque chose qui équilibre l'intuitivité, la cohérence avec le style de NUnit et la bonne quantité de flexibilité. (Pour la plupart, ces choses devraient déjà se chevaucher. )


Proposition A

```c#
[assemblage : FixtureOptions(InstancePerTestCase = true)]

[FixtureOptions (InstancePerTestCase = false)]
classe SomeFixture
{
}

Pro: extending later if there's ever more options for fixture handling (doubtful?)
Pro: two names so there's less stuffed into a single name
Con: `[FixtureOptions]` would be legal but highly misunderstandable syntax. (Does it reset the assembly-wide setting to the default, `false`, or does it have no effect, e.g. `null`?)


### Proposal B

```c#
[assembly: FixtureOptions(FixtureCreationOptions.PerTestCase)]

[FixtureOptions(FixtureCreationOptions.Singleton)]
class SomeFixture
{
}

public enum FixtureCreationOptions
{
    Singleton, // = default
    PerTestCase,
    // Doubtful there will be other options?
    // Maybe Pool, where we reset and reuse between nonconcurrent tests?    
}

Pro : les états invalides ne sont pas représentables
Pro : étendre plus tard s'il y a toujours plus d'options pour la gestion des appareils (douteux ?)
Inconvénient : redondance dans la saisie à la fois du nom de l'attribut et du nom de l'énumération

Proposition C

```c#
[assemblage : FixtureInstancePerTestCase]

[FixtureInstancePerTestCase(false)]
classe SomeFixture
{
}
```

Pro : les états invalides ne sont pas représentables
Pro : difficile de se méprendre (mais j'ai un angle mort ici car je ne suis pas un nouvel utilisateur)
Inconvénient : nom long
Peut-être con : Nom très spécifique qui peut spécifier à la fois l'option et la valeur. (Mais nous avons aussi [NonParallelizable] que j'aime bien.)


(Propositions alternatives bienvenues !)

Il y a quelque chose à dire pour avoir deux attributs : un pour l'assemblage et un pour la classe elle-même.

Avantages : Il est plus facile d'implémenter deux attributs simples plutôt qu'un seul attribut avec des comportements différents en contexte. (Leçon apprise de ParallelizableAttribute)
Avantage : Probablement plus facile à comprendre pour les utilisateurs, car chacun peut être nommé de manière appropriée. Par exemple, le niveau d'assemblage peut inclure le mot "Par défaut".
Inconvénient : Deux attributs à retenir.

Approchez-vous en utilisant chacune de vos alternatives :

```C#
[assembly : DefaultFixtureOptions(InstancePerTestCase = true)]

[FixtureOptions (InstancePerTestCase = false)]
classe SomeFixture
{
}

[assemblage : DefaultFixtureOptions(FixtureCreationOptions.PerTestCase)]

[FixtureOptions(FixtureCreationOptions.Singleton)]
classe SomeFixture
{
}

[assemblage : DefaultFixtureInstancePerTestCase]

[FixtureInstancePerTestCase(false)]
classe SomeFixture
{
}
```

Autres commentaires:

  • J'aime mieux A et B que C
  • Vous ne devriez pas appeler le comportement des appareils partagés "Singleton", car ce n'en est pas un. :smile: Si vous héritez, vous obtiendrez plusieurs instances de l'appareil de base.
  • Si vous utilisez des attributs distincts au niveau de l'assemblage et de l'appareil, vous pouvez en fait supprimer l'appareil du nom au niveau de l'appareil.
  • Nous n'avons pas du tout précisé comment SetUpFixtures s'insère dans cela. Ils sont problématiques et peuvent nécessiter une erreur d'exécution si l'attribut est utilisé sur eux. En fait, l'un des avantages d'un attribut entièrement séparé est que nous n'avons pas à nous en occuper. (C'est aussi un avantage pour une propriété de TestFixture, BTW)
  • Nous devons avertir les utilisateurs que l'instance par cas de test ne s'applique pas aux classesd statiques. C'est évident pour nous tous, mais il y a des gens qui ne le comprendront pas tout de suite.

BTW, j'ai souvent souhaité que nous allions avec DefaultTestCaseTimeOut pour les niveaux d'assemblage et de montage et que nous gardions Timeout uniquement pour les méthodes.

Bien, merci d'avoir évoqué cette possibilité.

Quelque chose qui me dérange en ce moment, c'est que cela pourrait théoriquement avoir du sens, même si nous n'allons pas dans cette direction :

c# [DefaultFixtureOptions(InstancePerTestCase = true)] class SomeFixture { [FixtureOptions(InstancePerTestCase = false)] public void SomeTest() { } }

Parce que le niveau auquel cela se joue n'est pas le niveau de l'appareil lui-même mais plutôt le niveau du cas de test.
Par exemple : un appareil dérivé peut remplacer le réglage de l'appareil de base, mais tout ce qu'il fait en réalité affecte le réglage de chaque cas de test produit par la classe dérivée.

Ou:

```c#
[assembly : DefaultFixtureOptions(InstancePerTestCase = true)]

// Pourquoi cela ne devrait-il pas aussi être considéré comme une valeur par défaut ?
[DefaultFixtureOptions (InstancePerTestCase = true)]
classe abstraite publique BaseFixture
{
}

classe scellée publique SomeFixture : BaseFixture
{
}
```

Comment trouver une justification pour quand et quand ne pas utiliser Default ?

Je pense que l'implémentation de cet attribut sur chaque cas de test introduirait une grande complexité avec seulement un petit avantage.

Pour les appareils de test "normaux", nous continuerons à instancier l'instance dans le cadre de la chaîne de commande associée à l'appareil de test. Pour les appareils d'instance par test, nous ne l'instancierons pas là mais devrons implémenter une nouvelle commande dans le cadre de la chaîne de test, ce qui fera à peu près la même chose que ce qui est fait maintenant dans la chaîne d'appareils.

Si un appareil peut contenir les deux types de test, alors nous devrons l'exécuter deux fois, une fois pour chaque modèle.

L'avantage semble faible, car l'utilisateur peut facilement obtenir la même chose en divisant les tests en appareils distincts. Les appareils, après tout, sont un artefact NUnit et il est raisonnable d'exiger des utilisateurs qu'ils mettent des tests qui nécessitent le même comportement d'appareil dans le même appareil.

En ce qui concerne l'utilisation de "Default"... il me semble que vous ne l'utiliseriez __pas__ sur n'importe quel TestFixxture et que vous ne l'utiliseriez qu'au niveau de l'assemblage. Les classes de base ne sont pas une exception car l'attribut se trouve en fait sur la classe dérivée en vertu de l'héritage. Ainsi, la définition du comportement particulier sur la base n'est pas une valeur par défaut. C'est plutôt le cadre de cette classe et de toutes celles qui en dérivent. Les spécifications contradictoires devraient soit provoquer une erreur, soit être ignorées, comme nous le faisons actuellement dans tous les cas d'attributs hérités. C'est-à-dire que le fait de placer l'attribut A sur la classe de base et B sur la classe dérivée ne se distingue pas du fait de placer A et B directement sur la classe de base. Nous l'avons conçu de cette façon car il serait surprenant pour l'utilisateur de faire autre chose.

Installations de configuration WRT, d'ailleurs, je pense que cela devrait générer une erreur d'exécution pour spécifier l'une ou l'autre option d'instanciation là-bas. Les SetUpFixtures ne sont pas des TestFixtures.

C'est-à-dire que le fait de placer l'attribut A sur la classe de base et B sur la classe dérivée ne se distingue pas du fait de placer A et B directement sur la classe de base. Nous l'avons conçu de cette façon car il serait surprenant pour l'utilisateur de faire autre chose.

Oh mon Dieu, je me serais attendu [Apartment] ce que [Parallelizable] remplacent le paramètre de la classe de base ou de la méthode de base !

Tout cela a du sens pour moi.

Pouvons-nous exclure de vouloir une énumération telle que FixtureCreationOptions ? PerTestCase , PerFixture , hypothétique ReusedPerTestCase ?

Je penche pour la simplicité de :

```c#
[assembly : DefaultFixtureOptions(InstancePerTestCase = true)]

[FixtureOptions (InstancePerTestCase = false)]
classe SomeFixture
{
}

Hypothetical other creation options would then be added this way:
```c#
[FixtureOptions(InstancePerTestCase = false, ReuseInstances = true)]

@nunit/framework-team Quelles sont vos préférences ?

Concernant

[assembly: DefaultFixtureOptions(InstancePerTestCase = true)]

Peut-être juste moi, mais généralement je comprends les attributs comme indiquant que l'objet marqué possède un certain trait, non présent autrement ; les arguments de l'attribut, le cas échéant, affinent ledit trait. Prenez, par exemple, Serializable : l'objet est sérialisable, mais s'il n'est pas décoré, il ne l'est pas ; InternalsVisibleTo--internals visibles pour quelque chose de spécifique, et les arguments affinent cela, sinon internal ne sont visibles pour rien, etc. Certains attributs ne suivent pas le modèle, mais ceux-ci sont généralement de bas niveau, e, g, CompilationRepresentation, MethodImpl. Ceux-ci viennent généralement avec les arguments de constructeur requis.

Considérez maintenant
c# [assembly: DefaultFixtureOptions]
C'est un C# bien formé, puisque l'affectation à InstancePerTestCase est facultative. Qu'exprime-t-il sémantiquement ?

Pour moi, [assembly: DefaultFixtureInstancePerTestCase] semble plus naturel (à une exception près pour son nom sesquipédalien, mais ce n'est qu'un avis, non justifiable sauf esthétiquement). Un attribut, un trait. Peut-être que celui-ci n'a pas besoin de paramètres ou de propriétés réglables en option.

De plus, en tant qu'utilisateur, je trouve surprenant qu'il y ait deux attributs avec des noms différents, l'un applicable uniquement au niveau de l'assemblage ( DefaultSomething ) et un autre uniquement au niveau du type (juste Something ) . Naturellement, je ne comprends pas quelle était la "leçon apprise" que @CharliePoole a mentionnée concernant l'attribut Parallelizable , mais de ce côté de l'île, une paire d'attributs pour apprendre où l'on ferait (dans la pensée superficielle de cet utilisateur) semble déroutant.

@kkm000 Je suis d'accord avec votre commentaire sur un attribut qui exprime un seul trait.

WRT deux attributs différents pour le "même" trait à différents niveaux, je serais d'accord si je pensais qu'ils étaient en fait les mêmes. Mais je ne pense pas qu'ils le soient. Je vais utiliser Timeout pour un exemple.

Si Timeout est placé sur une méthode de test, cela signifie "Cette méthode de test expirera si elle prend plus que le nombre de millisecondes spécifié pour s'exécuter." Cependant, lorsque Timeout est placé sur un appareil ou un assembly, il a une signification différente : « Le délai d'expiration par défaut pour tous les cas de test contenus dans cet élément est au nombre de millisecondes spécifié. » Cette valeur par défaut peut, bien sûr, être remplacée à des niveaux inférieurs. Deux significations assez différentes, qui ont d'ailleurs deux implémentations distinctes. Le premier applique le timeout au test sur lequel il apparaît. Le second stocke la valeur par défaut dans le contexte de test où elle sera trouvée et utilisée par défaut lorsque des cas de test contenus sont rencontrés. ParallelizableAttribute fonctionne de la même manière.

Dans ce cas particulier, placé sur un appareil, l'attribut signifie « Instancier cet appareil une fois par cas de test ». Au niveau de l'assemblage, cela signifie « Appareils de test instantané dans cet assemblage une fois par appareil, sauf indication contraire ».

Dans ce cas particulier, placé sur un appareil, l'attribut signifie « Instancier cet appareil une fois par cas de test ». Au niveau de l'assemblage, cela signifie « Appareils de test instantané dans cet assemblage une fois par appareil, sauf indication contraire ».

En supposant que vous vous êtes trompé dans la dernière phrase, était-ce que _"Les appareils de test instantanés dans cet assemblage une fois par scénario de test ~appareil~

En supposant que je ne sois pas, pour moi, en tant qu'utilisateur, la sémantique n'a pas l'air différente du tout : « instancier une classe par test dans la portée marquée par l'attribut », peut-être clarifié avec « à moins que sa portée intérieure ne soit marquée différemment, s'il y a est en effet une telle portée intérieure », qu'il s'agisse d'un accessoire ou de l'assemblage. Juste mon avis, mais deux attributs semblent un peu déroutants. C'est peut-être juste ma façon de penser, je ne sais pas vraiment.

deux implémentations distinctes

Dans le monde des licornes et des arcs-en-ciel, la mise en œuvre n'affecte pas le côté utilisateur. Mais nous (vous le concepteur NUnit et moi l'un de ses utilisateurs) sommes tous des ingénieurs, et l'ingénierie est une question de compromis. Si l'utilisation d'un attribut pour différentes portées entraînait en fait une lourdeur interne et une charge de maintenance, je ne grincerais pas du tout. :)

En fin de compte, pour moi, c'est un problème mineur, et je ne me sens pas fortement pour le même attribut et je peux vivre avec les attributs arrangés de toute façon.

@kkm000 Oui, je voulais "une fois par

Je suis d'accord que tout cela est parfaitement logique si vous le considérez comme une série de portées imbriquées. Cependant, l'expérience montre que les utilisateurs (a) ne pensent pas toujours de cette façon ou (b) ne comprennent pas correctement la définition de la portée.

Comme exemple du dernier, considérons les classes imbriquées et les classes de base. Ni l'un ni l'autre n'est traité par NUnit comme des "portées" imbriquées à des fins de test, mais ils peuvent sembler ainsi pour les utilisateurs.

J'admets que le problème que j'ai mentionné avec Timeout / DefaultTimeout est un peu plus grave, car Timeout sur un appareil peut facilement signifier que le délai s'applique à l'ensemble de l'appareil, ce qui n'est bien sûr pas le cas. D'ailleurs, Timeout sur l'assembly peut s'appliquer à l'ensemble de l'assembly, ce qui n'est pas le cas.

Dans ce cas, il semble y avoir moins d'ambiguïté, car nous utilisons "par cas de test" dans le nom afin que je puisse facilement aller dans les deux sens. Je ne travaille pas dessus de toute façon. :le sourire:

À bien y penser, si Timeout s'était appelé TestCaseTimeout, cela aurait été mieux !

Je suis content d'un seul [FixtureInstancePerTestCase] . Semble le moins fardeau mental sur les utilisateurs.

@nunit/framework-team Pouvez-vous nous aider à trouver une direction, en survolant https://github.com/nunit/nunit/issues/2574#issuecomment -426347735 ?

[FixtureInstancePerTestCase] est aussi mon option la moins appréciée. Le manque d'extensibilité est le seul inconvénient.

Je n'aime pas le problème [FixtureOptions] - je pense que c'est trop déroutant. Personnellement, je préférerais un seul attribut pour l'assembly et la classe - j'apprécie que cela nous ait déjà causé des problèmes pour Timeout et Paralelliizable - mais lorsque celui-ci a "Fixture" dans le nom, cela me semble moins ambigu.

Personnellement, je cite aimé l'option enum... sauf que je ne sais pas comment la nommer de manière appropriée pour la rendre plus succincte !

Y a-t-il une mise à jour à ce sujet ?

Toute mise à jour sera publiée sur ce problème.

Quelqu'un peut résoudre ce problème s'il vous plaît ??
Actuellement, nous devons envelopper tout notre code de test comme ceci :
csharp [Test] public void TestExample(){ using(var tc = new MyTestContext()){ //code goes here } }
En utilisant cette fonctionnalité, nous pouvons initialiser le contexte de test lors de la configuration et le supprimer lors du démontage tout en restant thread-safe.

Bonjour, nous rencontrons également ce problème dans notre entreprise. Nous migrons à partir d'un framework qui prend en charge cette fonctionnalité (Py.Test), c'est donc un peu un piège. Nous vous serions très reconnaissants si vous pouviez nous montrer le code source qui contrôle cette instanciation afin que nous puissions le forker et apporter les modifications nécessaires.

@MChiciak
Si ce problème ne sera pas résolu bientôt, nous envisageons de migrer vers xUnit, car dans xUnit, il s'agit du comportement par défaut : "xUnit.net crée une nouvelle instance de la classe de test pour chaque test exécuté" https://xunit .github.io/docs/shared-context

@LirazShay , @MChiciak J'ai également rencontré ce problème mais je suis d'accord avec les points de [ThreadStatic] et cela fonctionne bien.

@dfal
Merci pour la belle solution de contournement!

J'adorerais ça. J'ai actuellement une suite de tests existante avec des milliers de tests exécutés sur nunit, qui ne peuvent pas être parallélisés facilement en raison de l'état partagé. Si cette option était fournie, nous serions en mesure de paralléliser notre suite de tests beaucoup plus facilement.

En tant qu'expérience de pensée, imaginez que nous avons simplement changé le fonctionnement de TestFixture. Qu'est-ce que cela briserait ?

  1. Tous les tests qui utilisent l'ordre et s'appuient sur les changements d'état apportés par un test pour influencer d'autres tests. C'est une très mauvaise pratique dans le cas de tests unitaires (soi-disant) indépendants. Nous avons toujours averti les gens de ne pas le faire et cela ne me dérangerait pas de casser les tests (avec un message, bien sûr) qui reposaient sur une seule instance de cette manière.

  2. En fonction de ce qui est fait dans la configuration unique, il est possible que les instances d'appareil individuelles ne puissent pas fonctionner en parallèle. C'est ironique, puisque la motivation du changement est de permettre aux appareils de se dérouler en parallèle ! Ce n'est pas un changement décisif pour les personnes qui ont besoin de la fonctionnalité, puisqu'elles sont déjà incapables de l'exécuter en parallèle, mais c'est un changement important pour ceux qui comptent déjà sur l'exécution des tests en parallèle __après__ un `OneTimeSetUp' non parallélisable.

  3. Tout niveau d'appareil OneTimeSetUp serait désormais exécuté plusieurs fois. Cela éliminerait tout gain d'efficacité de la configuration unique et pourrait, dans certains cas, changer la sémantique. En particulier, si l'initialisation a affecté l'état global d'une manière ou d'une autre (par exemple, la configuration d'une base de données), cela peut entraîner des erreurs. Celui-ci me semble être une casse inacceptable.

J'ai trouvé cette expérience de réflexion utile pour réfléchir à la façon d'implémenter l'instanciation par cas de test des appareils. Il doit clairement s'agir d'une nouvelle fonctionnalité non par défaut si elle est introduite dans NUnit 3. Pour NUnit 4 ou tout autre nouveau framework (par exemple TestCentric), le nouveau comportement pourrait facilement être la valeur par défaut.

pour mémoire, j'ai très simplement remplacé certains TestClass et TestMethod MSTest par TestFixture et Test respectivement, et j'ai découvert que NUnit et MSTest ont ici une sémantique différente. MSTest crée de nouvelles instances pour chaque test où, comme je suis sûr que tout le monde le sait, NUnit réutilise les instances TestFixture.

Ce serait bien si je pouvais écrire TestFixture(lifeCycle = OneTestPerInstance)] ou similaire, pour faciliter cette migration.

_edit:_ En fait, après avoir porté les tests pour utiliser une méthode Setup , il s'avère que le code existant d'initialisation/avant/démontage a été assez torturé sous MSTest, et est maintenant de simples constructeurs d'initialisation de propriété, qui est beaucoup plus agréable et plus complet, donc je suis content d'avoir fait la traduction. Néanmoins, pour une question de compatibilité, ce serait bien de l'avoir.

@CharliePoole

Tous les tests qui utilisent l'ordre et s'appuient sur les changements d'état apportés par un test pour influencer d'autres tests.

Aah, oui, casse, casse ces tests, s'il te plait !!! C'est le pire abus d'un cadre de test unitaire auquel je puisse penser.

Tout niveau OneTimeSetUp luminaire

Euh, j'ai toujours pensé que cela était exécuté une fois par processus... Je me suis toujours demandé pourquoi ce n'était pas statique.

Mais oui, la performance est aussi un point très valable.

Cette fonctionnalité fait l'objet de discussions depuis plus de 2 ans maintenant, je ne veux pas paraître offensant, mais existe-t-il des plans concrets et réalistes pour la mettre en œuvre dans un avenir proche ?

J'aimerais savoir si mon équipe doit planifier une migration vers un autre framework ou si cela sera résolu dans les mois à venir, car ce problème est un écueil pour nous.

Il s'agit d'un changement majeur dans la façon dont les tests s'exécutent et il faudrait considérer tout l'impact afin de ne pas complètement casser ceux qui dépendent de la fonctionnalité actuelle.

La meilleure façon de "résoudre" ce problème est de sécuriser le thread de votre classe de test. Comme toute bonne application multithread, la sécurité des threads doit être primordiale.
Si vous utilisez par exemple des champs que vous configurez dans une méthode de configuration puis utilisés dans de nombreuses méthodes de test, votre classe n'est pas thread-safe. ce n'est pas un défaut de NUnit. Vous avez écrit un singleton non thread-safe.

En créant une sous-classe dans votre TestFixture qui contient les informations contextuelles que vous créez dans le cadre de chaque test, vous vous assurez alors que votre singleton TestFixture est threadsafe... ainsi... vous "résolvez" le problème.
Donc, plutôt que de passer du temps à "migrer vers un autre framework", je pourrais suggérer que vous ayez passé ce temps à rendre votre singleton TestFixtures threadsafe... vous trouverez probablement que c'est un exercice beaucoup plus rapide.

Par exemple, lorsque j'ai découvert pour la première fois qu'un NUnit TestFixture est un singleton, j'ai converti mes 100+ appareils de test avec plus de 500 tests pour être threadsafe (et j'ai dû passer de RhinoMocks à Moq car RhinoMocks n'est pas non plus threadsafe) et cela a pris moi environ 4 semaines (pour être honnête, Rhino -> Moq a pris la plupart de ce temps !)

Vous avez 100% raison en principe, mais lorsque vous avez une suite de tests existante de ~ 7000+ tests unitaires, il suffit de les réécrire pour être thread-safe est un investissement énorme.

Je ne sais pas si c'est plus un investissement que de passer à un autre framework (il peut y avoir un outil pour automatiser cela) mais je ne pense pas que cette demande de fonctionnalité devrait être rejetée par principe, IMO

Je ne préconise pas que cette fonctionnalité ne devrait pas être examinée.

Mais un si grand changement dans la façon dont nunit fonctionne, à mon avis devrait être pris en compte pour la compatibilité descendante (ce qui peut être assez difficile) ou la nouvelle version majeure (pas non plus une petite tâche)

Je suggère que faire en sorte que le code qu'un utilisateur de nunit écrive threadsafe pour s'exécuter dans un environnement multi-thread est une forme d'action plus simple. Vous pouvez peut-être même le scripter.

Je ne suis pas un contributeur à ce dépôt, donc je n'ai aucun contexte sur la façon dont il est implémenté. Cela peut être incroyablement naïf à cause de cela, mais ne serait-il pas possible de l'implémenter en tant que commutateur facultatif ? Pour que cela ne casse rien ?

Toute cette question traite de diverses manières d'implémenter un commutateur facultatif ... il existe de nombreuses façons

Il s'agit d'un projet open source entièrement composé de bénévoles. Une fois qu'une fonctionnalité est acceptée, telle qu'elle a été, elle attend que quelqu'un de l'équipe ou de l'extérieur le récupère et le fasse. Il n'y a pas de "patron" pour assigner le travail à qui que ce soit. C'est en fait une bonne chose que personne n'ait fait cela s'ils n'ont pas le temps à consacrer à le faire. De cette façon, il est disponible pour quiconque est capable et intéressé à le faire.

Les PR sont toujours les bienvenus, mais ce n'est probablement pas une première contribution pour quelqu'un, car cela a le potentiel de casser beaucoup d'autres choses dans NUnit. Mon avis en tout cas.

Je ne contribue plus aussi activement que par le passé, donc mes commentaires sur la façon dont je pense que cela devrait être fait ont été donnés à titre de conseil à quiconque le prend.

Je suis d'accord qu'il existe depuis un certain temps et j'aimerais le voir faire moi-même.

@BlythMeister Je pense qu'un commutateur le simplifie un peu - comme vous le savez probablement, mais d'autres ne le savent peut-être pas.

Bien sûr, c'est un interrupteur, mais il ne s'agit pas seulement d'allumer une ampoule. C'est plus comme faire passer votre radio de l'AM à la FM... des circuits entièrement nouveaux entrent en jeu.

Dans ce cas, il s'agit d'un tout nouveau cycle de vie dans l'exécution d'un test, donc une bonne quantité de code doit être remplacée. Pas une petite chose. Le risque sera de s'assurer que le nouveau "circuit" n'empiète en aucune façon sur l'ancien.

Désolé votre droit, mon explication était vague en eux pourquoi un « commutateur » n'est pas simple !

@BlythMeister J'ai en fait fait exactement ce que vous décrivez - créé un objet jetable pour suivre l'état pendant un test, puis l'éliminer une fois le test terminé - pour piloter mon harnais de test Selenium. Un problème majeur avec cette approche est que l'État ne persiste pas dans le démantèlement. Par conséquent, il n'est pas possible d'exécuter des actions de manière conditionnelle pour les tests ayant échoué (capture d'écrans, journaux ou autres éléments d'état) car l'état du test ne se résoudra pas dans la méthode de test elle-même et ne pourra être évalué que lors du démontage. Ainsi, bien qu'un objet d'état soit un moyen puissant de rendre les tests thread-safe, il ne fonctionne que lorsque vous n'avez pas besoin de faire des choses avec le résultat du test pendant le démontage.

Il est utile de garder à l'esprit que faire des choses avec le résultat du test n'est pas ce pour quoi TearDown a été conçu. Bien sûr, je sais que beaucoup de gens l'utilisent de cette façon et je comprends la motivation, mais il y a beaucoup de cas difficiles qui se résument essentiellement au fait que TearDown fait partie du test et qu'il n'est pas terminé tant qu'il n'est pas terminé.

Avec NUnit 3, nous espérions voir plus d'utilisateurs examiner les résultats en dehors du cadre à l'aide d'extensions de moteur, mais travailler dans le test lui-même semble être plus familier et accessible.

@CharliePoole

  1. Tout niveau d'appareil OneTimeSetUp serait désormais exécuté plusieurs fois. Cela éliminerait tout gain d'efficacité de la configuration unique et pourrait, dans certains cas, changer la sémantique. En particulier, si l'initialisation a affecté l'état global d'une manière ou d'une autre (par exemple, la configuration d'une base de données), cela peut entraîner des erreurs. Celui-ci me semble être une casse inacceptable.

Pourquoi ne pouvons-nous pas implémenter la fonctionnalité Instance-per-test-case et exécuter le OneTimeSetUp une seule fois (gardez le thread principal n'oubliez pas de l'appeler ou quelque chose) ?

Parce que cela casserait tout OneTimeSetUp existant qui a initialisé l'instance, ce qui est très courant.

Je vois cela comme un problème dans toute solution basée sur un commutateur. C'est moins un problème si nous définissons une toute nouvelle alternative au montage de test, une approche que je trouve assez tentante.

Parce que cela casserait tout OneTimeSetUp existant qui a initialisé l'instance, ce qui est très courant.

Que diriez-vous d'autoriser uniquement les méthodes statiques OneTimeSetUp et OneTimeTearDown lorsque la fonctionnalité « instance par cas de test » est activée ? Cela forcerait les membres initialisés dans ces méthodes à être statiques, donc évidemment partagés entre les instances de fixture.

Bien que la principale motivation de ce problème Github soit la parallélisation, j'aimerais également inclure le _principe de la moindre surprise_ et _éviter les erreurs causées par les tests qui s'influencent mutuellement_ comme motivations. (Je m'excuse si j'ai raté que quelqu'un d'autre l'ait déjà mentionné.)

Au cas où vous n'auriez pas lu assez loin dans ce fil assez ancien, mes commentaires ne contestent pas la fonctionnalité, mais contre une implémentation particulière qui a été suggérée. Je préfère remplacer TestFixture par un tout nouveau type de luminaire, qui fonctionne différemment.

Je pense que le principe de moindre surprise fonctionne bien ici. Il n'est pas surprenant qu'un nouveau luminaire fonctionne différemment.

Si, d'un autre côté, nous créons un drapeau qui contrôle le projecteur existant, je peux penser à cinq ou six endroits différents dans l'implémentation actuelle où nous aurions besoin de brancher sur ce drapeau.

Cela ne veut pas dire que les deux types d'appareils pourraient ne pas partager certains codes. Mais ce serait en dehors de la vue des utilisateurs.

@fschmied BTW, si nous faisions un nouveau framework (ou peut-être même NUnit 4), alors j'argumenterais différemment... Je pense que nous aurions dû rendre beaucoup plus difficile l'interférence des tests dans NUnit 2 et 3. Cependant , tant que nous apportons des modifications à NUnit 3, je pense aux règles de compatibilité.

Je préfère remplacer TestFixture par un tout nouveau type de luminaire, qui fonctionne différemment.
[...]
Je pense que le principe de moindre surprise fonctionne bien ici. Il n'est pas surprenant qu'un nouveau luminaire fonctionne différemment.

Tu as raison bien sur. Ce que je voulais dire, c'est que pour le type d'appareil d'essai original, le comportement sera toujours surprenant pour la plupart des gens. (Cela a surpris un collègue il y a deux semaines, lorsqu'il a découvert un cas de test en influençant un autre. Il utilise NUnit depuis 2013, c'est pourquoi j'ai recherché ce problème.)

Mais bien sûr, c'est un compromis entre la rétrocompatibilité, la complexité de l'implémentation et le souhait de corriger un péché originel [1].

Revenons au problème de OneTimeSetUp : Je pense qu'il est toujours important d'avoir un OneTimeSetUp par appareil de test, même si la classe d'appareil de test est instanciée par test. Pour éviter toute confusion au cas où OneTimeSetUp manipule l'état de l'instance, je proposerais d'appliquer OneTimeSetUp (et OneTimeTearDown ) pour qu'il soit statique avec la fonctionnalité d'instance par test. (Peu importe s'il est implémenté en tant que commutateur ou en tant que nouveau type de montage de test.)


[1] James Newkirk a écrit ceci :

Je pense que l'un des plus gros ratés qui a été fait lorsque nous avons écrit NUnit V2.0 était de ne pas créer une nouvelle instance de la classe de montage de test pour chaque méthode de test contenue. Je dis "nous" mais je pense que celui-ci était de ma faute. Je n'ai pas bien compris le raisonnement dans JUnit pour créer une nouvelle instance du dispositif de test pour chaque méthode de test.

Il écrit aussi ceci, cependant :

[...] il serait difficile de changer la façon dont NUnit fonctionne maintenant, trop de gens se plaindraient [...]

:)

Oui, Jim et moi nous disputons à ce sujet depuis des années. :smile: Une chose sur laquelle nous sommes d'accord est la dernière phrase. Jim est parti créer un nouveau cadre avant de mettre en œuvre son approche préférée. Je fais essentiellement la même chose et SetUp/TearDown est une chose que je vais probablement faire différemment.

Je continue d'être surpris du nombre d'utilisateurs assez expérimentés de NUnit qui ne sont pas au courant de l'utilisation de la même instance pour tous les cas de test. Il se peut qu'il soit plus connu lorsque NUnit est sorti pour la première fois, car il s'agissait d'une différence évidente avec JUnit. Maintenant, tout cela est oublié et de nouvelles personnes viennent à NUnit avec des attentes auxquelles NUnit ne correspond pas. Quelque chose à quoi penser.

alors.. est-ce à gagner ? ou encore en phase de conception ?

en ce moment, dans .NET, son XUnit sans TestContext ou NUnit, avec des tests parallèles passe-partout

La fonction est acceptée et une priorité normale lui est attribuée. Personne n'y est affecté et il n'est pas répertorié sur un jalon. Cela signifie que quelqu'un doit décider qu'il veut y travailler. Cela peut être un commiter ou un contributeur.

Nous ne faisons généralement pas de phase de conception - si nous en voulons une, le label de conception est appliqué - mais même ainsi, celui qui le fait serait bien avisé de publier un commentaire décrivant comment il envisage de le mettre en œuvre, en particulier à quoi il ressemblera utilisateurs. Si vous deviez commencer par le coder, vous pourriez de toute façon vous retrouver dans des discussions de conception, il est donc probablement préférable d'en finir d'abord.

Alors, vous aimeriez y travailler ?

bien sûr, je viens de commencer à travailler à
https://github.com/avilv/nunit/tree/instance-per-test

Je suis nouveau dans cette base de code, alors j'essaie de m'en tenir au style de code d'origine autant que possible
jusqu'à présent, j'ai introduit un attribut InstancePerTestCase et je prévois de le prendre en charge au niveau de l'assemblage et de la classe

faites-moi savoir si je suis loin de la base ici

J'ai ajouté des commentaires sur certains détails d'implémentation sur votre dernier commit.

En ce qui concerne la conception, je pense qu'un attribut dérivé de IApplyToContext sens. Je pense que je préférerais quelque chose de plus général que juste InstancePerTestCase, peut-être

C# [FixtureLifeCycle(LifeCycle.InstancePerTestCase]

Cela vous permettrait de le définir au niveau de l'assemblage et de le réinitialiser sur des appareils individuels.

CharliePoole vous êtes trop, merci beaucoup pour les commentaires et l'aide

je pense que je comprends comment les choses sont construites / qu'est-ce qui fait quoi, etc. lentement mais sûrement

voici ma proposition pour l'énumération LifeCycle (ive a également mis à jour ma branche avec votre suggestion)

    /// <summary>
    /// Specifies the lifecycle for a fixture.
    /// </summary>
    public enum LifeCycle
    {
        /// <summary>
        /// A single instance is created and for all test cases.
        /// </summary>
        SingleInstance,

        /// <summary>
        /// A new instance is created for each test case for fixtures that are marked with <see cref="ParallelizableAttribute"/>.
        /// </summary>
        InstancePerTestCaseForParallelFixtures,

        /// <summary>
        /// A new instance is created for each test case.
        /// </summary>
        InstancePerTestCase
    }

@jnm2 Que pensez-vous de cette approche - vous êtes en quelque sorte le "sponsor" de cette fonctionnalité en premier lieu. Je suis un peu incertain à propos de InstancePerTestCaseForParallelFixtures , ce qui pourrait être un peu trop pour une implémentation initiale. Comme d'habitude, une fois implémenté, on s'y colle.

Si nous __do__ acceptons le lien avec le parallélisme, il doit être basé sur le contexte et non sur les attributs. C'est-à-dire que n'importe quel test peut être exécuté en parallèle, il ressemble donc plus à `InstancePerTestCaseWhenCasesAreParallel. Les appareils parallèles ne nécessitent pas particulièrement cette fonctionnalité. Et peut-être vaut-il mieux laisser l'utilisateur décider.

Je suis également d'accord qu'il ne doit en aucun cas être associé au parallélisme. le parallélisme était la raison d'ajouter cette fonctionnalité mais une fois que nous sommes d'accord que cette fonctionnalité est utile, à mon avis, elle ne devrait pas être reflétée dans le code

je suis d'accord, je comprends le cas d'utilisation pour l'utiliser uniquement dans des scénarios parallèles puisque c'est là que le modèle par défaut actuel de NUnit vous mordra, mais j'espère que ce sera le cycle de vie par défaut à l'avenir (nunit 4?) c'est comme ça depuis le début

peut-être devrions-nous le laisser à SingleInsance/InstancePerTestCase

une autre question est de savoir si nous devons gérer le modèle IDisposable ici, cela rendrait à peu près SetUp/TearDown redondant mais semblerait beaucoup plus naturel à la manière du style XUnit.

Nous disposons déjà de tout appareil de test qui implémente IDisposable. Vous avez juste besoin de vous assurer que cela continue de fonctionner.

super, fera l'affaire.

Je vais probablement commencer à ajouter des cas de test aujourd'hui, mais comme il s'agit d'un mécanisme de base, cela pourrait ajouter pas mal de cas qui garantissent simplement que la même chose fonctionne lorsque InstancePerTestCase est activé, corrigez-moi si je me trompe

La DisposeFixtureCommand s'occupe d'appeler Dispose.

OMI, votre principal problème est de vous assurer que l'intégralité du flux de commandes pour le dispositif de test est invoqué __pour chaque test__. Les changements que je vois jusqu'à présent créent correctement un flux de commandes qui semble faire le travail, mais la question clé est de savoir comment s'assurer qu'il est invoqué pour chaque scénario de test. (Notez que la structure de commande pour les appareils est distincte de celle des cas de test.) Je pense que compositeworkitem doit être sensible au contexte et appeler la commande correctement (comme vous l'avez fait) mais je ne suis pas sûr qu'il aura une chance pour le faire pour __chaque__ cas de test. (Je ne fais que lire le code, donc je peux me tromper.)

@CharliePoole semble avoir raison, j'ai fait un petit graphique sur ce qui exécute exactement une méthode de test :
image

actuellement, je crée l'instance d'objet de test dans la boucle RunChildren, mais cela ne suffira pas pour les commandes Retry/Repeat, sans parler du fait qu'il y a d'autres wrappers que j'ai manqués/quelqu'un en a besoin de nouveaux.
il va falloir être beaucoup plus proche de TestMethodCommand

j'ai mis à jour les derniers changements sur
https://github.com/avilv/nunit/tree/instance-per-test-2

supprimé InstancePerTestCaseWhenCasesAreParallel
implémenté FixtureLifeCycle en ajoutant un luminaire Construct/Dispose dans MakeTestCommand dans SimpleWorkItem et en l'ignorant dans CompositeWorkItem MakeOneTimeSetUpCommand

cela semble bien fonctionner, cela a également plus de sens avec la façon dont le cadre est construit
maintenant j'ai juste besoin d'ajouter un tas de tests unitaires pour m'assurer que tout fonctionne correctement

une chance que cela soit revu avant la prochaine version ? je suis toujours prêt pour des améliorations/changements de cours

Cette approche me semble bonne et j'adorerais l'utiliser dans la prochaine version si nous pouvons le faire !

une chance que nous puissions faire réviser cela? j'ai l'impression que nous nous rapprochons vraiment

Curieux de savoir s'il y a eu du mouvement sur cette fonctionnalité ? On dirait que @avilv s'en est assez bien rapproché. J'attendais une fonctionnalité comme celle-ci depuis longtemps et je suis impatient de l'utiliser !

Merci a tous!

Cette fonctionnalité fonctionne-t-elle maintenant ?

@CharliePoole, pourriez-vous revoir la dernière version de @avilv ? Il semble être pleinement mis en œuvre. Je pense que beaucoup de gens attendent cette fonctionnalité.

@janissimsons Désolé, mais je ne suis plus sur ce projet, donc mon avis ne le ferait pas avancer. @nunit-framework-team Quelqu'un ne peut-il pas réviser cela?

Appréciez tout le travail qui a été fait pour fusionner #3427. @rprouse une idée de quand nous pouvons nous attendre à cela dans une version ?

Comment utilisons-nous la nouvelle fonctionnalité ? Quelqu'un pourrait-il rédiger une brève description des nouveaux attributs et comment les utiliser correctement ?

Je prévois de sortir dans les prochaines semaines. Je voulais attendre. NET 5 est final et effectuez quelques tests finaux avec avant sa sortie. Je doute qu'il y ait des problèmes, mais c'est si proche que j'ai pensé qu'il valait mieux attendre.

@rprouse Avez-vous une estimation de la date de sortie de la nouvelle version ? J'aimerais commencer à utiliser cette fonctionnalité.

@janissimsons très bientôt. Nous travaillons sur certains problèmes avec nos versions .NET 5.0 et quelques autres problèmes, puis je publierai. Vous pouvez suivre l'état sur ce tableau de projet, https://github.com/nuni/nuni/projects/4

C'est une merveilleuse nouvelle !

Dois-je bien comprendre que cela rendra possible ce qui suit dans NUnit ?

Combinez tout cela :

  • Les tests sont paramétrés
  • Exécuter des tests en parallèle.
  • Être capable d'exécuter des variations paramétrées du même test en parallèle.
  • Les tests peuvent être pilotés par les données

J'ai essayé certaines choses dans le passé ici https://github.com/jawn/dotnet-parallel-parameterized-tests

La nouvelle version sera-t-elle toujours compatible avec .NET Full Framework 4.7.2 ou au moins 4.8 ?

Meilleures salutations,
RD

@jawn Je crois que tout cela fonctionnera, oui. J'ai testé toutes ces choses, mais NUnit a de nombreuses fonctionnalités, il peut donc y avoir des cas limites avec certaines combinaisons de fonctionnalités. N'hésitez pas à retirer notre forfait NuGet nocturne et à le tester ! Nous aimerions plus de tests et de commentaires avant de sortir.

@drauch oui, la version 3.13 prendra toujours en charge le retour à .NET 3.5.

@avilv Excellent travail !!
au fait, avec quel outil as-tu créé ce joli diagramme de flux ?
Merci

@rprouse Pourriez-vous également mettre à jour la documentation sur l'utilisation de cette nouvelle fonctionnalité ? Merci!

@janissimsons Je [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] à votre classe de test, probablement avec [Parallelizable(ParallelScope.All)] et une instance de votre classe de test sera instanciée pour chaque test exécuté dans la classe. Cela vous permet d'utiliser des variables membres dans la classe dans des tests parallèles sans vous soucier des autres tests qui modifient les variables.

@LirazShay Le diagramme de la capture d'écran a été créé dans Visual Studio Enterprise. Vous pouvez cliquer avec le bouton droit sur une sélection dans l'Explorateur de solutions et choisir « Afficher sur la carte de code », par exemple. https://docs.microsoft.com/en-us/visualstudio/modeling/map-dependencies-across-your-solutions ReSharper a une fonctionnalité similaire.

@jnm2 Merci beaucoup !!!
J'ai VS Enterprise et je ne connaissais pas cette fonctionnalité qui peut être très utile
Merci beaucoup pour l'astuce

Ai-je raison de dire que cela a été publié et fait partie de la version 3.13 ?

Ai-je raison de dire que cela a été publié et fait partie de la version 3.13 ?

Je pense qu'il y a eu quelques problèmes observés et corrigés, en particulier concernant l'application d'attributs au niveau de l'assemblage dans #3720 et nous attendons maintenant la version 3.13.1.

NUnit 3.13.1 a été publié.
Plus de problèmes restants ?

Non, c'est pourquoi il a été fermé @andrewlaser

Cette page vous a été utile?
0 / 5 - 0 notes