Déterminer si le code s'exécute dans le cadre d'un test unitaire

105

J'ai un test unitaire (nUnit). De nombreuses couches dans la pile d'appels, une méthode échouera si elle s'exécute via un test unitaire.

Idéalement, vous utiliseriez quelque chose comme la moquerie pour configurer l'objet dont dépend cette méthode, mais il s'agit d'un code tiers et je ne peux pas le faire sans beaucoup de travail.

Je ne veux pas configurer des méthodes spécifiques à nUnit - il y a trop de niveaux ici et c'est une mauvaise façon de faire des tests unitaires.

Au lieu de cela, je voudrais ajouter quelque chose comme ça au plus profond de la pile d'appels

#IF DEBUG // Unit tests only included in debug build
if (IsRunningInUnitTest)
   {
   // Do some setup to avoid error
   }
#endif

Alors des idées sur la façon d'écrire IsRunningInUnitTest?

PS Je suis pleinement conscient que ce n'est pas un design génial, mais je pense que c'est mieux que les alternatives.

Ryan
la source
5
Vous ne devez pas tester directement ou indirectement du code tiers dans un test unitaire. Vous devez isoler votre méthode testée de l'implémentation tierce.
Craig Stuntz
14
Oui - je m'en rends compte - dans un monde d'idées, mais parfois nous devons être un peu pragmatiques sur les choses non?
Ryan
9
Revenant au commentaire de Craig - pas sûr que ce soit vrai. Si ma méthode repose sur le comportement de la bibliothèque tierce d'une certaine manière, cela ne devrait-il pas faire partie du test? Si l'application tierce change, je souhaite que mon test échoue. Si vous utilisez des simulations de vos tests par rapport à la façon dont vous pensez que l'application tierce fonctionne, pas à la manière dont elle fonctionne réellement.
Ryan
2
Ryan, vous pouvez tester des hypothèses sur le comportement d'un tiers, mais c'est un test distinct. Vous devez tester votre propre code de manière isolée.
Craig Stuntz
2
Je comprends ce que vous dites, mais pour tout sauf un exemple trivial, vous parleriez d'une grande (énorme) quantité de travail et rien ne garantit que les hypothèses que vous vérifiez dans votre test sont les mêmes que vos hypothèses dans vos méthodes réelles. . Hmm - débat pour un article de blog, je pense, je vous enverrai un e-mail quand j'aurai mes pensées ensemble.
Ryan

Réponses:

80

Je l'ai déjà fait - j'ai dû me tenir le nez pendant que je le faisais, mais je l'ai fait. Le pragmatisme l'emporte à chaque fois sur le dogmatisme. Bien sûr, s'il est un moyen agréable , vous pouvez factoriser pour l' éviter, ce serait formidable.

Fondamentalement, j'avais une classe "UnitTestDetector" qui vérifiait si l'assembly du framework NUnit était chargé dans l'AppDomain actuel. Il ne fallait le faire qu'une seule fois, puis mettre en cache le résultat. Moche, mais simple et efficace.

Jon Skeet
la source
un échantillon sur UnitTestDetector? et similaire pour MSTest?
Kiquenet le
4
@Kiquenet: Je pense que je voudrais simplement utiliser AppDomain.GetAssemblieset vérifier l'assemblage pertinent - pour MSTest, vous devez regarder quels assemblages sont chargés. Regardez la réponse de Ryan pour un exemple.
Jon Skeet
Ce n'est pas une bonne approche pour moi. J'appelle une méthode UnitTest à partir d'une application console et il pense que c'est une application UnitTest.
Bizhan
1
@Bizhan: Je dirais alors que vous êtes dans une situation plutôt spécialisée, et vous ne devriez pas vous attendre à ce que des réponses plus générales fonctionnent. Vous voudrez peut-être poser une nouvelle question avec toutes vos exigences spécifiques. (Quelle est la différence entre "votre code appelant depuis une application console" et "un exécuteur de test" par exemple? Comment voudriez-vous faire la distinction entre votre application console et tout autre exécuteur de test basé sur la console?)
Jon Skeet
74

En prenant l'idée de Jon, c'est ce que j'ai trouvé -

using System;
using System.Reflection;

/// <summary>
/// Detect if we are running as part of a nUnit unit test.
/// This is DIRTY and should only be used if absolutely necessary 
/// as its usually a sign of bad design.
/// </summary>    
static class UnitTestDetector
{

    private static bool _runningFromNUnit = false;      

    static UnitTestDetector()
    {
        foreach (Assembly assem in AppDomain.CurrentDomain.GetAssemblies())
        {
            // Can't do something like this as it will load the nUnit assembly
            // if (assem == typeof(NUnit.Framework.Assert))

            if (assem.FullName.ToLowerInvariant().StartsWith("nunit.framework"))
            {
                _runningFromNUnit = true;
                break;
            }
        }
    }

    public static bool IsRunningFromNUnit
    {
        get { return _runningFromNUnit; }
    }
}

À l'arrière, nous sommes tous assez grands pour reconnaître quand nous faisons quelque chose que nous ne devrions probablement pas;)

Ryan
la source
2
+1 Bonne réponse. Cela peut être un peu simplifié, voir ci-dessous: stackoverflow.com/a/30356080/184528
cdiggins
Le projet particulier pour lequel j'ai écrit ceci était (et est toujours!) .NET 2.0 donc pas de linq.
Ryan
Cela fonctionne pour moi, mais il semble que le nom de l'assembly ait changé depuis. Je suis passé à la solution de
Kiquenet
J'ai dû désactiver la journalisation pour les versions de travis ci, cela a tout gelé
jjxtra
Fonctionne pour moi, je dois pirater les bogues .NET core 3 avec rasoir qui ne se produisent que dans les tests unitaires.
jjxtra
62

Adapté de la réponse de Ryan. Celui-ci est pour le cadre de test unitaire MS.

La raison pour laquelle j'en ai besoin est que je montre une MessageBox sur les erreurs. Mais mes tests unitaires testent également le code de gestion des erreurs, et je ne veux pas qu'un MessageBox apparaisse lors de l'exécution des tests unitaires.

/// <summary>
/// Detects if we are running inside a unit test.
/// </summary>
public static class UnitTestDetector
{
    static UnitTestDetector()
    {
        string testAssemblyName = "Microsoft.VisualStudio.QualityTools.UnitTestFramework";
        UnitTestDetector.IsInUnitTest = AppDomain.CurrentDomain.GetAssemblies()
            .Any(a => a.FullName.StartsWith(testAssemblyName));
    }

    public static bool IsInUnitTest { get; private set; }
}

Et voici un test unitaire pour cela:

    [TestMethod]
    public void IsInUnitTest()
    {
        Assert.IsTrue(UnitTestDetector.IsInUnitTest, 
            "Should detect that we are running inside a unit test."); // lol
    }
dan-gph
la source
8
J'ai un meilleur moyen de résoudre votre problème MessageBox et d'annuler ce hack et de fournir plus de cas de test unitaires. J'utilise une classe qui implémente une interface que j'appelle ICommonDialogs. La classe d'implémentation affiche toutes les boîtes de dialogue contextuelles (MessageBox, boîtes de dialogue de fichier, sélecteur de couleurs, boîte de dialogue de connexion à la base de données, etc.). Les classes qui doivent afficher des boîtes de message acceptent ICommonDiaglogs comme paramètre de constructeur que nous pouvons ensuite simuler dans le test unitaire. Bonus: vous pouvez vous affirmer sur les appels MessageBox attendus.
Tony O'Hagan
1
@Tony, bonne idée. C'est clairement la meilleure façon de procéder. Je ne sais pas à quoi je pensais à l'époque. Je pense que l'injection de dépendance était encore nouvelle pour moi à cette époque.
dan-gph
3
Sérieusement, les gens apprennent l'injection de dépendance et, accessoirement, se moquent des objets. L'injection de dépendances va révolutionner votre programmation.
dan-gph
2
J'implémenterai UnitTestDetector.IsInUnitTest comme "return true" et votre test unitaire passera. ;) Une de ces choses amusantes qui semble impossible à tester unitaire.
Samer Adra
1
Microsoft.VisualStudio.QualityTools.UnitTestFramework ne fonctionnait plus pour moi. Changé en Microsoft.VisualStudio.TestPlatform.TestFramework - cela fonctionne à nouveau.
Alexander le
22

Pour simplifier la solution de Ryan, vous pouvez simplement ajouter la propriété statique suivante à n'importe quelle classe:

    public static readonly bool IsRunningFromNUnit = 
        AppDomain.CurrentDomain.GetAssemblies().Any(
            a => a.FullName.ToLowerInvariant().StartsWith("nunit.framework"));
cdiggins
la source
2
À peu près la même chose que la réponse de dan-gph (même si cela cherchait le jeu d'outils VS, pas la nunit).
Ryan
18

J'utilise une approche similaire à celle de tallseth

C'est le code de base qui pourrait être facilement modifié pour inclure la mise en cache. Une autre bonne idée serait d'ajouter un setter IsRunningInUnitTestet d'appeler UnitTestDetector.IsRunningInUnitTest = falsele point d'entrée principal de vos projets pour éviter l'exécution du code.

public static class UnitTestDetector
{
    public static readonly HashSet<string> UnitTestAttributes = new HashSet<string> 
    {
        "Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute",
        "NUnit.Framework.TestFixtureAttribute",
    };
    public static bool IsRunningInUnitTest
    {
        get
        {
            foreach (var f in new StackTrace().GetFrames())
                if (f.GetMethod().DeclaringType.GetCustomAttributes(false).Any(x => UnitTestAttributes.Contains(x.GetType().FullName)))
                    return true;
            return false;
        }
    }
}
Jürgen Steinblock
la source
J'aime cette approche plus que les réponses les plus votées. Je ne pense pas qu'il soit sûr de supposer que les assemblys de test unitaire ne seront jamais chargés que pendant un test unitaire et le nom du processus peut également varier d'un développeur à l'autre (par exemple, certains utilisent le testeur R #).
EM0
Cette approche fonctionnerait, mais elle recherchera ces attributs chaque fois que le getter IsRunningInUnitTest est appelé. Dans certains cas, cela peut affecter les performances. La vérification de AssemblyName est moins chère car elle n'est effectuée qu'une seule fois. L'idée avec le setter public est bonne mais dans ce cas, la classe UnitTestDetector doit être placée dans un assembly partagé.
Sergey
13

Peut-être utile, en vérifiant le ProcessName actuel:

public static bool UnitTestMode
{
    get 
    { 
        string processName = System.Diagnostics.Process.GetCurrentProcess().ProcessName;

        return processName == "VSTestHost"
                || processName.StartsWith("vstest.executionengine") //it can be vstest.executionengine.x86 or vstest.executionengine.x86.clr20
                || processName.StartsWith("QTAgent");   //QTAgent32 or QTAgent32_35
    }
}

Et cette fonction doit également être vérifiée par unittest:

[TestClass]
public class TestUnittestRunning
{
    [TestMethod]
    public void UnitTestRunningTest()
    {
        Assert.IsTrue(MyTools.UnitTestMode);
    }
}

Références:
Matthew Watson dans http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/11e68468-c95e-4c43-b02b-7045a52b407e/

Kiquenet
la source
|| processName.StartsWith("testhost") // testhost.x86pour VS 2019
Kiquenet
9

En mode test, Assembly.GetEntryAssembly()semble être null.

#IF DEBUG // Unit tests only included in debug build 
  if (Assembly.GetEntryAssembly() == null)    
  {
    // Do some setup to avoid error    
  }
#endif 

Notez que si Assembly.GetEntryAssembly()c'est null, Assembly.GetExecutingAssembly()non.

La documentation dit: La GetEntryAssemblyméthode peut être renvoyée nulllorsqu'un assembly géré a été chargé à partir d'une application non gérée.

Eric Bole-Feysot
la source
8

Quelque part dans le projet testé:

public static class Startup
{
    public static bool IsRunningInUnitTest { get; set; }
}

Quelque part dans votre projet de test unitaire:

[TestClass]
public static class AssemblyInitializer
{
    [AssemblyInitialize]
    public static void Initialize(TestContext context)
    {
        Startup.IsRunningInUnitTest = true;
    }
}

Élégant, non. Mais simple et rapide. AssemblyInitializerest pour MS Test. Je m'attendrais à ce que d'autres cadres de test aient des équivalents.

Edward Brey
la source
1
Si le code que vous testez crée des AppDomains supplémentaires, il IsRunningInUnitTestn'est pas défini sur true dans ces AppDomains.
Edward Brey
Mais cela pourrait être facilement résolu en ajoutant un assembly partagé ou en déclarant IsRunningInUnitTest dans chaque domaine.
Sergey
3

Je l'utilise uniquement pour ignorer la logique qui désactive tous les TraceAppenders dans log4net au démarrage lorsqu'aucun débogueur n'est connecté. Cela permet aux tests unitaires de se connecter à la fenêtre de résultats Resharper même lorsqu'ils sont exécutés en mode non débogué.

La méthode qui utilise cette fonction est appelée soit au démarrage de l'application, soit au démarrage d'un appareil de test.

Il est similaire à l'article de Ryan mais utilise LINQ, supprime l'exigence System.Reflection, ne cache pas le résultat et est privé pour éviter toute utilisation abusive (accidentelle).

    private static bool IsNUnitRunning()
    {
        return AppDomain.CurrentDomain.GetAssemblies().Any(assembly => assembly.FullName.ToLowerInvariant().StartsWith("nunit.framework"));
    }
Graeme Wicksted
la source
3

Avoir une référence au framework nunit ne signifie pas que le test est en cours d'exécution. Par exemple, dans Unity, lorsque vous activez les tests de mode de lecture, les références de l'unité sont ajoutées au projet. Et lorsque vous exécutez un jeu, les références existent, donc UnitTestDetector ne fonctionnera pas correctement.

Au lieu de vérifier l'assemblage de nunit, nous pouvons demander à l'api de nunit de vérifier si le code est en cours d'exécution maintenant ou non.

using NUnit.Framework;

// ...

if (TestContext.CurrentContext != null)
{
    // nunit test detected
    // Do some setup to avoid error
}

Éditer:

Attention, le TestContext peut être généré automatiquement s'il est nécessaire.

ChessMax
la source
2
Veuillez ne pas simplement vider le code ici. Expliquez ce qu'il fait.
nkr le
3

Utilisez simplement ceci:

AppDomain.CurrentDomain.IsDefaultAppDomain()

En mode test, il retournera faux.

brillance
la source
1

J'étais mécontent d'avoir ce problème récemment. Je l'ai résolu d'une manière légèrement différente. Premièrement, je n'étais pas disposé à faire l'hypothèse que le framework nunit ne serait jamais chargé en dehors d'un environnement de test; J'étais particulièrement préoccupé par les développeurs exécutant l'application sur leurs machines. J'ai donc parcouru la pile d'appels à la place. Deuxièmement, j'ai pu supposer que le code de test ne serait jamais exécuté sur les binaires de publication, donc je me suis assuré que ce code n'existait pas dans un système de publication.

internal abstract class TestModeDetector
{
    internal abstract bool RunningInUnitTest();

    internal static TestModeDetector GetInstance()
    {
    #if DEBUG
        return new DebugImplementation();
    #else
        return new ReleaseImplementation();
    #endif
    }

    private class ReleaseImplementation : TestModeDetector
    {
        internal override bool RunningInUnitTest()
        {
            return false;
        }
    }

    private class DebugImplementation : TestModeDetector
    {
        private Mode mode_;

        internal override bool RunningInUnitTest()
        {
            if (mode_ == Mode.Unknown)
            {
                mode_ = DetectMode();
            }

            return mode_ == Mode.Test;
        }

        private Mode DetectMode()
        {
            return HasUnitTestInStack(new StackTrace()) ? Mode.Test : Mode.Regular;
        }

        private static bool HasUnitTestInStack(StackTrace callStack)
        {
            return GetStackFrames(callStack).SelectMany(stackFrame => stackFrame.GetMethod().GetCustomAttributes(false)).Any(NunitAttribute);
        }

        private static IEnumerable<StackFrame> GetStackFrames(StackTrace callStack)
        {
            return callStack.GetFrames() ?? new StackFrame[0];
        }

        private static bool NunitAttribute(object attr)
        {
            var type = attr.GetType();
            if (type.FullName != null)
            {
                return type.FullName.StartsWith("nunit.framework", StringComparison.OrdinalIgnoreCase);
            }
            return false;
        }

        private enum Mode
        {
            Unknown,
            Test,
            Regular
        }
tallseth
la source
Je trouve que l'idée de tester la version de débogage lors de l'expédition de la version finale est une mauvaise idée en général.
Patrick M
1

fonctionne comme un charme

if (AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.FullName.ToLowerInvariant().StartsWith("nunit.framework")) != null)
{
    fileName = @"C:\Users\blabla\xxx.txt";
}
else
{
    var sfd = new SaveFileDialog
    {     ...     };
    var dialogResult = sfd.ShowDialog();
    if (dialogResult != DialogResult.OK)
        return;
    fileName = sfd.FileName;
}

.

Tom Nobleman
la source
1

Les tests unitaires ignoreront le point d'entrée de l'application. Au moins pour wpf, winforms et l'application console main()ne sont pas appelés.

Si la méthode main est appelée, nous sommes en exécution , sinon nous sommes en mode test unitaire :

public static bool IsUnitTest { get; private set; } = true;

[STAThread]
public static void main()
{
    IsUnitTest = false;
    ...
}
Sinatr
la source
0

Considérant que votre code est normalement exécuté dans le thread principal (gui) d'une application Windows Forms et que vous voulez qu'il se comporte différemment lors de l'exécution d'un test, vous pouvez vérifier

if (SynchronizationContext.Current == null)
{
    // code running in a background thread or from within a unit test
    DoSomething();
}
else
{
    // code running in the main thread or any other thread where
    // a SynchronizationContext has been set with
    // SynchronizationContext.SetSynchronizationContext(synchronizationContext);
    DoSomethingAsync();
}

J'utilise ceci pour le code que je veux fire and forgotdans une application d'interface graphique, mais dans les tests unitaires, je pourrais avoir besoin du résultat calculé pour une assertion et je ne veux pas jouer avec plusieurs threads en cours d'exécution.

Fonctionne pour MSTest. L'avantage est que mon code n'a pas besoin de vérifier le cadre de test lui-même et si j'ai vraiment besoin du comportement asynchrone dans un certain test, je peux définir mon propre SynchronizationContext.

Sachez que ce n'est pas une méthode fiable Determine if code is running as part of a unit testcomme demandé par OP car le code peut s'exécuter dans un thread, mais pour certains scénarios, cela pourrait être une bonne solution (aussi: si je suis déjà en cours d'exécution à partir d'un thread d'arrière-plan, cela peut ne pas être nécessaire pour en créer un nouveau).

Jürgen Steinblock
la source
0

Application Le courant est nul lors de l'exécution sous le testeur d'unité. Au moins pour mon application WPF utilisant le testeur MS Unit. C'est un test facile à faire si nécessaire. Aussi, quelque chose à garder à l'esprit lorsque vous utilisez Application.Current dans votre code.

Glaucus
la source
0

J'ai utilisé ce qui suit dans VB dans mon code pour vérifier si nous sommes dans un test unitaire. spécifiquement, je ne voulais pas que le test ouvre Word

    If Not Application.ProductName.ToLower().Contains("test") then
        ' Do something 
    End If
TomShantisoft
la source
0

Que diriez-vous d'utiliser la réflexion et quelque chose comme ça:

var underTest = Assembly.GetCallingAssembly ()! = typeof (MainForm) .Assembly;

L'assembly appelant sera l'endroit où se trouvent vos cas de test et remplacera simplement MainForm un type qui se trouve dans votre code en cours de test.

Jon
la source
-3

Il existe également une solution très simple lorsque vous testez une classe ...

Donnez simplement à la classe que vous testez une propriété comme celle-ci:

// For testing purposes to avoid running certain code in unit tests.
public bool thisIsUnitTest { get; set; }

Maintenant, votre test unitaire peut définir le booléen "thisIsUnitTest" sur true, donc dans le code que vous souhaitez ignorer, ajoutez:

   if (thisIsUnitTest)
   {
       return;
   } 

C'est plus facile et plus rapide que d'inspecter les assemblages. Cela me rappelle Ruby On Rails où vous chercheriez si vous êtes dans l'environnement TEST.

Danmar Herholdt
la source
1
Je pense que vous avez été contre-voté ici parce que vous comptez sur le test lui-même pour modifier le comportement de la classe.
Riegardt Steyn
1
Ce n'est pas pire que toutes les autres réponses ici.
DonO