Exécuter des tests unitaires en série (plutôt qu'en parallèle)

99

J'essaye de tester unitaire un moteur de gestion d'hôte WCF que j'ai écrit. Le moteur crée essentiellement des instances ServiceHost à la volée en fonction de la configuration. Cela nous permet de reconfigurer dynamiquement les services disponibles sans avoir à tous les arrêter et à les redémarrer chaque fois qu'un nouveau service est ajouté ou un ancien est supprimé.

Cependant, j'ai rencontré une difficulté lors des tests unitaires de ce moteur de gestion d'hôte en raison du fonctionnement de ServiceHost. Si un ServiceHost a déjà été créé, ouvert et pas encore fermé pour un point de terminaison particulier, un autre ServiceHost pour le même point de terminaison ne peut pas être créé, ce qui entraîne une exception. En raison du fait que les plates-formes de test unitaire modernes parallélisent leur exécution de test, je n'ai aucun moyen efficace de tester unité ce morceau de code.

J'ai utilisé xUnit.NET, en espérant qu'en raison de son extensibilité, je pourrais trouver un moyen de le forcer à exécuter les tests en série. Cependant, je n'ai pas eu de chance. J'espère que quelqu'un ici sur SO a rencontré un problème similaire et sait comment faire exécuter des tests unitaires en série.

REMARQUE: ServiceHost est une classe WCF, écrite par Microsoft. Je n'ai pas la capacité de changer son comportement. Héberger chaque point de terminaison de service une seule fois est également le bon comportement ... cependant, ce n'est pas particulièrement propice aux tests unitaires.

jrista
la source
Ce comportement particulier de ServiceHost ne serait-il pas quelque chose que vous voudriez peut-être aborder?
Robert Harvey
ServiceHost est écrit par Microsoft. Je n'ai aucun contrôle dessus. Et techniquement parlant, c'est un comportement valide ... vous ne devriez jamais avoir plus d'un ServiceHost par point de terminaison.
jrista
1
J'ai eu un problème similaire en essayant d'exécuter plusieurs TestServerdans docker. J'ai donc dû sérialiser les tests d'intégration.
h-rai

Réponses:

116

Chaque classe de test est une collection de tests unique et les tests sous-jacents seront exécutés en séquence, donc si vous placez tous vos tests dans la même collection, elle s'exécutera séquentiellement.

Dans xUnit, vous pouvez apporter les modifications suivantes pour y parvenir:

Ce qui suit fonctionnera en parallèle:

namespace IntegrationTests
{
    public class Class1
    {
        [Fact]
        public void Test1()
        {
            Console.WriteLine("Test1 called");
        }

        [Fact]
        public void Test2()
        {
            Console.WriteLine("Test2 called");
        }
    }

    public class Class2
    {
        [Fact]
        public void Test3()
        {
            Console.WriteLine("Test3 called");
        }

        [Fact]
        public void Test4()
        {
            Console.WriteLine("Test4 called");
        }
    }
}

Pour le rendre séquentiel, il vous suffit de placer les deux classes de test sous la même collection:

namespace IntegrationTests
{
    [Collection("Sequential")]
    public class Class1
    {
        [Fact]
        public void Test1()
        {
            Console.WriteLine("Test1 called");
        }

        [Fact]
        public void Test2()
        {
            Console.WriteLine("Test2 called");
        }
    }

    [Collection("Sequential")]
    public class Class2
    {
        [Fact]
        public void Test3()
        {
            Console.WriteLine("Test3 called");
        }

        [Fact]
        public void Test4()
        {
            Console.WriteLine("Test4 called");
        }
    }
}

Pour plus d'informations, vous pouvez vous référer à ce lien

Abhinav Saxena
la source
24
Réponse sous-estimée, je pense. Semble fonctionner et j'aime la granularité, car j'ai des tests parallélisables et non parallélisables dans un seul assemblage.
Igand
1
C'est la bonne façon de faire cela, référez-vous à la documentation Xunit.
Håkon K. Olafsen
2
Cela devrait être la réponse acceptée car généralement certains tests peuvent être exécutés en parallèle (dans mon cas, tous les tests unitaires), mais certains échouent de manière aléatoire lorsqu'ils sont exécutés en parallèle (dans mon cas, ceux qui utilisent un client / serveur Web en mémoire), donc on est capable d'optimiser le test en cours si l'on le souhaite.
Alexei du
2
Cela n'a pas fonctionné pour moi dans un projet principal .net où j'effectue des tests d'intégration avec une base de données sqlite. Les tests étaient toujours exécutés en parallèle. La réponse acceptée a cependant fonctionné.
user1796440
Merci beaucoup pour cette réponse! J'avais besoin de le faire car j'ai des tests d'acceptation dans différentes classes qui héritent à la fois de la même TestBase et la concurrence ne fonctionnait pas bien avec EF Core.
Kyanite
104

Comme indiqué ci-dessus, tous les bons tests unitaires doivent être isolés à 100%. L'utilisation de l'état partagé (par exemple en fonction d'une staticpropriété modifiée par chaque test) est considérée comme une mauvaise pratique.

Cela dit, votre question sur l'exécution des tests xUnit en séquence a une réponse! J'ai rencontré exactement le même problème car mon système utilise un localisateur de service statique (ce qui est loin d'être idéal).

Par défaut, xUnit 2.x exécute tous les tests en parallèle. Cela peut être modifié par assemblage en définissant le CollectionBehaviordans votre AssemblyInfo.cs dans votre projet de test.

Pour la séparation par assemblage, utilisez:

using Xunit;
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]

ou pour aucune parallélisation, utilisez:

[assembly: CollectionBehavior(DisableTestParallelization = true)]

Ce dernier est probablement celui que vous souhaitez. Vous trouverez plus d'informations sur la parallélisation et la configuration dans la documentation de xUnit .

Gribouiller
la source
5
Pour moi, il y avait des ressources partagées entre les méthodes au sein de chaque classe. Exécuter un test à partir d'une classe, puis l'un à partir d'une autre, interromprait les tests des deux. J'ai pu résoudre en utilisant [assembly: CollectionBehavior(CollectionBehavior.CollectionPerClass, DisableTestParallelization = true)]. Grâce à vous, @Squiggle, je peux faire tous mes tests et aller prendre un café! :)
Alielson Piffer
2
La réponse d'Abhinav Saxena est plus précise pour .NET Core.
Yennefer
67

Pour les projets .NET Core, créez xunit.runner.jsonavec:

{
  "parallelizeAssembly": false,
  "parallelizeTestCollections": false
}

En outre, votre csprojdevrait contenir

<ItemGroup>
  <None Update="xunit.runner.json"> 
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
</ItemGroup>

Pour les anciens projets .Net Core, votre project.jsondevrait contenir

"buildOptions": {
  "copyToOutput": {
    "include": [ "xunit.runner.json" ]
  }
}
Dmitry Polomoshnov
la source
2
Je suppose que le dernier équivalent de csproj dotnet core serait <ItemGroup><None Include="xunit.runner.json" CopyToOutputDirectory="Always" /></ItemGroup>ou similaire?
Squiggle
3
Cela a fonctionné pour moi dans csproj:<ItemGroup> <None Update="xunit.runner.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>
skynyrd
La désactivation de la parallélisation fonctionne-t-elle avec les théories xUnit?
John Zabroski
C'est la seule chose qui a fonctionné pour moi, j'ai essayé de courir dotnet test --no-build -c Release -- xunit.parallelizeTestCollections=falsemais ça n'a pas fonctionné pour moi.
Harvey
18

Pour les projets .NET Core, vous pouvez configurer xUnit avec un xunit.runner.jsonfichier, comme indiqué sur https://xunit.github.io/docs/configuring-with-json.html .

Le paramètre que vous devez modifier pour arrêter l'exécution des tests parallèles est parallelizeTestCollections, par défaut true:

Définissez ceci sur truesi l'assembly est prêt à exécuter des tests à l'intérieur de cet assembly en parallèle les uns par rapport aux autres. ... Définissez cette option sur falsepour désactiver toute la parallélisation dans cet assemblage de test.

Type de schéma JSON: booléen
Valeur par défaut:true

Donc, un minimum xunit.runner.jsonà cet effet ressemble à

{
    "parallelizeTestCollections": false
}

Comme indiqué dans la documentation, n'oubliez pas d'inclure ce fichier dans votre build, soit en:

  • Définition de Copier dans le répertoire de sortie sur Copier si plus récent dans les propriétés du fichier dans Visual Studio, ou
  • Ajouter

    <Content Include=".\xunit.runner.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>

    à votre .csprojdossier, ou

  • Ajouter

    "buildOptions": {
      "copyToOutput": {
        "include": [ "xunit.runner.json" ]
      }
    }

    à votre project.jsondossier

en fonction de votre type de projet.

Enfin, en plus de ce qui précède, si vous utilisez Visual Studio, assurez-vous que vous n'avez pas accidentellement cliqué sur le bouton Exécuter les tests en parallèle , ce qui entraînera l'exécution des tests en parallèle même si vous avez désactivé la parallélisation dans xunit.runner.json. Les concepteurs d'interface utilisateur de Microsoft ont astucieusement rendu ce bouton sans étiquette, difficile à remarquer et à environ un centimètre du bouton "Tout exécuter" dans l'Explorateur de tests, juste pour maximiser les chances que vous le frappiez par erreur et que vous ne sachiez pas pourquoi vos tests échouent soudainement:

Capture d'écran avec le bouton encerclé

Mark Amery
la source
@JohnZabroski Je ne comprends pas votre suggestion de modification . Qu'est-ce que ReSharper a à voir avec quoi que ce soit? Je pense que je l'avais probablement installé lorsque j'ai écrit la réponse ci-dessus, mais tout n'est-il pas ici indépendant du fait que vous l'utilisiez ou non? Qu'est-ce que la page vers laquelle vous créez un lien dans la modification a à voir avec la spécification d'un xunit.runner.jsonfichier? Et qu'est-ce que la spécification d'un xunit.runner.jsona à voir avec l'exécution des tests en série?
Mark Amery
J'essaie de faire exécuter mes tests en série, et j'ai d'abord pensé que le problème était lié à ReSharper (puisque ReSharper n'a PAS le bouton "Exécuter les tests en parallèle" comme le fait l'Explorateur de tests Visual Studio). Cependant, il semble que lorsque j'utilise [Théorie], mes tests ne sont pas isolés. C'est étrange, car tout ce que je lis suggère qu'une classe est la plus petite unité parallélisable.
John Zabroski
9

C'est une vieille question mais je voulais écrire une solution aux personnes qui recherchent nouvellement comme moi :)

Remarque: j'utilise cette méthode dans les tests d'intégration Dot Net Core WebUI avec xunit version 2.4.1.

Créez une classe vide nommée NonParallelCollectionDefinitionClass, puis attribuez l'attribut CollectionDefinition à cette classe comme ci-dessous. (La partie importante est DisableParallelization = true setting.)

using Xunit;

namespace WebUI.IntegrationTests.Common
{
    [CollectionDefinition("Non-Parallel Collection", DisableParallelization = true)]
    public class NonParallelCollectionDefinitionClass
    {
    }
}

Ensuite, ajoutez l'attribut Collection à la classe que vous ne voulez pas qu'il exécute en parallèle comme ci-dessous. (La partie importante est le nom de la collection. Il doit être identique au nom utilisé dans CollectionDefinition)

namespace WebUI.IntegrationTests.Controllers.Users
{
    [Collection("Non-Parallel Collection")]
    public class ChangePassword : IClassFixture<CustomWebApplicationFactory<Startup>>
    ...

Lorsque nous faisons cela, tout d'abord d'autres tests parallèles sont exécutés. Après cela, les autres tests qui ont l'attribut Collection ("Non-Parallel Collection") s'exécutent.

Gündüz Emre ÖZER
la source
6

vous pouvez utiliser la liste de lecture

clic droit sur la méthode de test -> Ajouter à la playlist -> Nouvelle playlist

alors vous pouvez spécifier l'ordre d'exécution, la valeur par défaut est, lorsque vous les ajoutez à la liste de lecture, mais vous pouvez changer le fichier de liste de lecture comme vous le souhaitez

entrez la description de l'image ici

HB MAAM
la source
5

Je ne connais pas les détails, mais il semble que vous essayez de faire des tests d'intégration plutôt que des tests unitaires . Si vous pouviez isoler la dépendance sur ServiceHost, cela rendrait probablement vos tests plus faciles (et plus rapides). Ainsi (par exemple) vous pouvez tester les éléments suivants indépendamment:

  • Classe de lecture de configuration
  • ServiceHost factory (éventuellement comme test d'intégration)
  • Classe de moteur qui prend un IServiceHostFactoryet unIConfiguration

Des outils qui aideraient à inclure des frameworks d'isolation (mocking) et (éventuellement) des frameworks de conteneurs IoC. Voir:

TrueWill
la source
Je n'essaye pas de faire des tests d'intégration. J'ai en effet besoin de faire des tests unitaires. Je connais parfaitement les termes et pratiques TDD / BDD (IoC, DI, Mocking, etc.), donc le genre de choses comme la création d'usines et l'utilisation d'interfaces n'est pas ce dont j'ai besoin (c'est déjà fait, sauf dans le cas de ServiceHost lui-même.) ServiceHost n'est pas une dépendance qui peut être isolée, car il n'est pas correctement mockable (autant d'espaces de noms du système .NET.) J'ai vraiment besoin d'un moyen d'exécuter les tests unitaires en série.
jrista
1
@jrista - aucune erreur sur vos compétences n'était prévue. Je ne suis pas un développeur WCF, mais serait-il possible pour le moteur de renvoyer un wrapper autour de ServiceHost avec une interface sur le wrapper? Ou peut-être une usine personnalisée pour les hôtes de service?
TrueWill
Le moteur d'hébergement ne renvoie aucun ServiceHosts. En fait, il ne renvoie rien, il gère simplement la création, l'ouverture et la fermeture de ServiceHosts en interne. Je pourrais encapsuler tous les types fondamentaux de WCF, mais c'est BEAUCOUP de travail que je n'ai pas vraiment été autorisé à faire. En outre, il s'est avéré que le problème n'est pas causé par une exécution parallèle et se produira toujours pendant le fonctionnement normal. J'ai commencé une autre question ici sur SO sur le problème, et j'espère que j'obtiendrai une réponse.
jrista le
@TrueWill: BTW, je ne craignais pas du tout que vous méprisiez mes compétences ... Je ne voulais tout simplement pas obtenir beaucoup de réponses banales qui couvrent toutes les choses courantes sur les tests unitaires. J'avais besoin d'une réponse rapide sur un problème très spécifique. Désolé si je suis sorti un peu bourru, ce n'était pas mon intention. J'ai juste assez de temps limité pour faire fonctionner cette chose.
jrista le
3

Vous pouvez peut-être utiliser Advanced Unit Testing . Il vous permet de définir la séquence dans laquelle vous exécutez le test . Vous devrez peut-être créer un nouveau fichier cs pour héberger ces tests.

Voici comment plier les méthodes de test pour qu'elles fonctionnent dans l'ordre souhaité.

[Test]
[Sequence(16)]
[Requires("POConstructor")]
[Requires("WorkOrderConstructor")]
public void ClosePO()
{
  po.Close();

  // one charge slip should be added to both work orders

  Assertion.Assert(wo1.ChargeSlipCount==1,
    "First work order: ChargeSlipCount not 1.");
  Assertion.Assert(wo2.ChargeSlipCount==1,
    "Second work order: ChargeSlipCount not 1.");
  ...
}

Faites-moi savoir si cela fonctionne.

Graviton
la source
Excellent article. Je l'ai en fait mis en signet sur CP. Merci pour le lien, mais il s'est avéré que le problème semble être beaucoup plus profond, car les testeurs ne semblent pas exécuter de tests en parallèle.
jrista le
2
Attendez, d'abord vous dites que vous ne voulez pas que le test s'exécute en parallèle, puis vous dites que le problème est que les testeurs n'exécutent pas de tests en parallèle ... alors lequel est lequel?
Graviton
Le lien que vous avez fourni ne fonctionne plus. Et est-ce quelque chose que vous pouvez faire avec xunit?
Allen Wang
1

Pour moi, dans l'application .Net Core Console, lorsque je voulais exécuter des méthodes de test (pas des classes) de manière synchrone, la seule solution qui fonctionnait était celle décrite dans ce blog: xUnit: contrôler l'ordre d'exécution des tests

user3057544
la source
0

J'ai ajouté l'attribut [Collection ("Sequential")] dans une classe de base:

    namespace IntegrationTests
    {
      [Collection("Sequential")]
      public class SequentialTest : IDisposable
      ...


      public class TestClass1 : SequentialTest
      {
      ...
      }

      public class TestClass2 : SequentialTest
      {
      ...
      }
    }
Matías Romero
la source
0

Aucune des réponses suggérées jusqu'à présent n'a fonctionné pour moi. J'ai une application de base dotnet avec XUnit 2.4.1. J'ai obtenu le comportement souhaité avec une solution de contournement en plaçant un verrou dans chaque test unitaire à la place. Dans mon cas, je ne me souciais pas de l'ordre de marche, juste que les tests étaient séquentiels.

public class TestClass
{
    [Fact]
    void Test1()
    {
        lock (this)
        {
            //Test Code
        }
    }

    [Fact]
    void Test2()
    {
        lock (this)
        {
            //Test Code
        }
    }
}
Matt Hayes
la source