Determina se il codice viene eseguito come parte di uno unit test


105

Ho uno unit test (nUnit). Molti livelli più in basso nello stack di chiamate un metodo fallirà se viene eseguito tramite uno unit test.

Idealmente useresti qualcosa come la presa in giro per impostare l'oggetto da cui dipende questo metodo, ma questo è un codice di terze parti e non posso farlo senza molto lavoro.

Non voglio impostare metodi specifici per nUnit: ci sono troppi livelli qui ed è un modo scadente di eseguire il test unitario.

Invece quello che vorrei fare è aggiungere qualcosa di simile in fondo allo stack di chiamate

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

Quindi qualche idea su come scrivere IsRunningInUnitTest?

PS Sono pienamente consapevole che questo non è un ottimo design, ma penso che sia migliore delle alternative.


5
Non dovresti testare direttamente o indirettamente codice di terze parti in uno unit test. È necessario isolare il metodo sottoposto a test dall'implementazione di terze parti.
Craig Stuntz

14
Sì - me ne rendo conto - in un mondo di idee, ma a volte dobbiamo essere un po 'pragmatici sulle cose, no?
Ryan

9
Tornando al commento di Craig, non sono sicuro che sia vero. Se il mio metodo si basa su una libreria di terze parti che si comporta in un certo modo, non dovrebbe far parte del test? Se l'app di terze parti cambia, desidero che il mio test fallisca. Se stai usando il tuo test deride su come pensi che l'app di terze parti funzioni, non su come funziona effettivamente.
Ryan

2
Ryan, puoi testare le ipotesi sul comportamento di terze parti, ma questo è un test separato. Devi testare il tuo codice in isolamento.
Craig Stuntz

2
Capisco quello che dici, ma per qualsiasi cosa tranne che per un banale esempio parleresti di una grande (enorme) quantità di lavoro e non c'è nulla per garantire che le ipotesi che stai controllando nel tuo test siano le stesse delle tue ipotesi nei tuoi metodi effettivi . Hmm - dibattito per un post sul blog Penso, ti manderò un'e-mail quando avrò riunito i miei pensieri.
Ryan

Risposte:


80

L'ho già fatto prima - ho dovuto tapparmi il naso mentre lo facevo, ma l'ho fatto. Il pragmatismo batte ogni volta il dogmatismo. Naturalmente, se v'è un bel modo è possibile refactoring per evitarlo, sarebbe fantastico.

Fondamentalmente avevo una classe "UnitTestDetector" che controllava se l'assembly del framework NUnit era stato caricato nell'AppDomain corrente. È stato necessario eseguire questa operazione solo una volta, quindi memorizzare nella cache il risultato. Brutto, ma semplice ed efficace.


qualsiasi campione su UnitTestDetector? e simili per MSTest?
Kiquenet

4
@Kiquenet: penso che userei AppDomain.GetAssembliese controllerei l'assembly pertinente - per MSTest dovresti guardare quali assembly sono caricati. Guarda la risposta di Ryan per un esempio.
Jon Skeet

Questo non è un buon approccio per me. Chiamo un metodo UnitTest da un'app console e pensa che sia un'app UnitTest.
Bizhan

1
@Bizhan: allora ti suggerirei di trovarti in una situazione piuttosto specializzata e non dovresti aspettarti che risposte più generali funzionino. Potresti voler fare una nuova domanda con tutte le tue esigenze specifiche. (Qual è la differenza tra "il tuo codice che chiama da un'applicazione console" e "un test runner", ad esempio? Come vorresti distinguere tra la tua applicazione console e qualsiasi altro test runner basato su console?)
Jon Skeet

74

Prendendo l'idea di Jon, questo è ciò che mi è venuto in mente -

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; }
    }
}

Sdraiati sul retro siamo tutti abbastanza grandi da riconoscere quando stiamo facendo qualcosa che probabilmente non dovremmo;)


2
+1 Buona risposta. Questo può essere semplificato un po ', però, vedere sotto: stackoverflow.com/a/30356080/184528
cdiggins

Il particolare progetto per cui ho scritto questo era (ed è ancora!) .NET 2.0, quindi niente linq.
Ryan

Questo uso per me funziona, ma sembra che il nome dell'assembly sia cambiato da allora. Sono passato alla soluzione di
Kiquenet

Ho dovuto disattivare la registrazione per le build di travis ci, ha bloccato tutto
jjxtra

Per me funziona, devo hackerare i bug di .NET Core 3 con il rasoio che si verificano solo nei test unitari.
jjxtra

62

Adattato dalla risposta di Ryan. Questo è per il framework di unit test MS.

Il motivo per cui ho bisogno di questo è perché mostro un MessageBox in caso di errori. Ma i miei unit test testano anche il codice di gestione degli errori e non voglio che venga visualizzato un MessageBox durante l'esecuzione di unit test.

/// <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; }
}

Ed ecco un test unitario per questo:

    [TestMethod]
    public void IsInUnitTest()
    {
        Assert.IsTrue(UnitTestDetector.IsInUnitTest, 
            "Should detect that we are running inside a unit test."); // lol
    }

8
Ho un modo migliore per risolvere il tuo problema MessageBox e annullare questo hack e fornire più casi di unit test. Uso una classe che implementa un'interfaccia che chiamo ICommonDialogs. La classe di implementazione mostra tutte le finestre di dialogo a comparsa (MessageBox, Finestre di dialogo File, Selettore colore, finestra di dialogo di connessione al database, ecc.). Le classi che devono visualizzare le finestre di messaggio accettano ICommonDiaglogs come parametro del costruttore che possiamo quindi simulare nello unit test. Bonus: puoi far valere le chiamate MessageBox previste.
Tony O'Hagan

1
@ Tony, buona idea. Questo è chiaramente il modo migliore per farlo. Non so cosa stavo pensando in quel momento. Penso che l'iniezione di dipendenza fosse ancora una novità per me a quel tempo.
dan-gph

3
Seriamente, gente, impara a conoscere l'iniezione di dipendenza e, in secondo luogo, a simulare oggetti. L'inserimento delle dipendenze rivoluzionerà la tua programmazione.
dan-gph

2
Implementerò UnitTestDetector.IsInUnitTest come "return true" e il tuo unit test passerà. ;) Una di quelle cose divertenti che sembra impossibile da testare.
Samer Adra

1
Microsoft.VisualStudio.QualityTools.UnitTestFramework non funzionava più per me. Modificato in Microsoft.VisualStudio.TestPlatform.TestFramework: funziona di nuovo.
Alexander

21

Semplificando la soluzione di Ryan, puoi semplicemente aggiungere la seguente proprietà statica a qualsiasi classe:

    public static readonly bool IsRunningFromNUnit = 
        AppDomain.CurrentDomain.GetAssemblies().Any(
            a => a.FullName.ToLowerInvariant().StartsWith("nunit.framework"));

2
Più o meno la stessa della risposta di dan-gph (anche se stava cercando il set di strumenti VS, non nunit).
Ryan

18

Uso un approccio simile a tallseth

Questo è il codice di base che potrebbe essere facilmente modificato per includere la memorizzazione nella cache. Un'altra buona idea sarebbe quella di aggiungere un setter IsRunningInUnitTeste chiamare UnitTestDetector.IsRunningInUnitTest = falseal punto di ingresso principale del progetto per evitare l'esecuzione del codice.

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;
        }
    }
}

Mi piace questo approccio più delle risposte più votate. Non penso sia sicuro presumere che gli assembly di unit test verranno caricati solo durante uno unit test e il nome del processo può anche variare da sviluppatore a sviluppatore (ad esempio, alcuni usano il runner di test R #).
EM0

Questo approccio funzionerebbe ma cercherà quegli attributi ogni volta che viene chiamato il getter IsRunningInUnitTest . Potrebbero esserci dei casi in cui può influire sulle prestazioni. Il controllo di AssemblyName è più economico perché viene eseguito una sola volta. L'idea con il public setter è buona, ma in questo caso, la classe UnitTestDetector dovrebbe essere inserita in un assembly condiviso.
Sergey

13

Forse utile, controllando il ProcessName corrente:

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
    }
}

E questa funzione dovrebbe essere verificata anche da unittest:

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

Riferimenti:
Matthew Watson in http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/11e68468-c95e-4c43-b02b-7045a52b407e/


|| processName.StartsWith("testhost") // testhost.x86per VS 2019
Kiquenet

9

In modalità test, Assembly.GetEntryAssembly()sembra essere null.

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

Nota che se Assembly.GetEntryAssembly()è null,Assembly.GetExecutingAssembly() non lo è.

La documentazione dice: Il GetEntryAssemblymetodo può restituire nullquando un assembly gestito è stato caricato da un'applicazione non gestita.


8

Da qualche parte nel progetto in fase di test:

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

Da qualche parte nel tuo progetto di unit test:

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

Elegante, no. Ma semplice e veloce. AssemblyInitializerè per MS Test. Mi aspetto che altri framework di test abbiano equivalenti.


1
Se il codice che stai testando crea AppDomain aggiuntivi, IsRunningInUnitTestnon viene impostato su true in quegli AppDomain.
Edward Brey

Ma potrebbe essere facilmente risolto aggiungendo un assembly condiviso o dichiarando IsRunningInUnitTest in ogni dominio.
Sergey

3

Uso solo questo per saltare la logica che disabilita tutti i TraceAppender in log4net durante l'avvio quando non è collegato alcun debugger. Ciò consente agli unit test di accedere alla finestra dei risultati di Resharper anche durante l'esecuzione in modalità non di debug.

Il metodo che utilizza questa funzione viene chiamato all'avvio dell'applicazione o quando si avvia un dispositivo di prova.

È simile al post di Ryan, ma utilizza LINQ, elimina il requisito System.Reflection, non memorizza il risultato nella cache ed è riservato per prevenire un uso improprio (accidentale).

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

3

Avere un riferimento al framework nunit non significa che il test sia effettivamente in esecuzione. Ad esempio, in Unity, quando si attivano i test in modalità di riproduzione, i riferimenti nunit vengono aggiunti al progetto. E quando esegui un gioco i riferimenti esistono, quindi UnitTestDetector non funzionerebbe correttamente.

Invece di controllare l'assembly nunit, possiamo chiedere a nunit api di verificare se il codice è in esecuzione test ora o no.

using NUnit.Framework;

// ...

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

Modificare:

Attenzione che il TestContext può essere generato automaticamente se necessario.


2
Per favore, non scaricare il codice qui. Spiega cosa fa.
nkr

2

Usa questo:

AppDomain.CurrentDomain.IsDefaultAppDomain()

In modalità test, restituirà false.


1

Non ero contento di avere questo problema di recente. L'ho risolto in un modo leggermente diverso. Innanzitutto, non ero disposto a supporre che il framework nunit non sarebbe mai stato caricato al di fuori di un ambiente di test; Ero particolarmente preoccupato per gli sviluppatori che eseguivano l'app sui loro computer. Quindi ho camminato invece sullo stack di chiamate. In secondo luogo, sono stato in grado di supporre che il codice di test non sarebbe mai stato eseguito contro i binari di rilascio, quindi mi sono assicurato che questo codice non esistesse in un sistema di rilascio.

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
        }

Trovo che l'idea di testare la versione di debug durante la spedizione della versione di rilascio sia una cattiva idea in generale.
Patrick M

1

funziona come un fascino

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;
}

.


1

I test unitari salteranno il punto di ingresso dell'applicazione. Almeno per wpf, winforms e applicazione consolemain() non vengono chiamate.

Se viene chiamato il metodo principale di quello che siamo in fase di esecuzione , altrimenti siamo in modalità di test unitario :

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

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

0

Considerando che il tuo codice è normalmente eseguito nel thread principale (gui) di un'applicazione Windows Form e vuoi che si comporti in modo diverso durante l'esecuzione in un test puoi controllare

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();
}

Lo sto usando per il codice che voglio fire and forgotin un'applicazione gui, ma negli unit test potrei aver bisogno del risultato calcolato per un'asserzione e non voglio fare confusione con più thread in esecuzione.

Funziona per MSTest. Il vantaggio è che il mio codice non ha bisogno di controllare il framework di test stesso e se ho davvero bisogno del comportamento asincrono in un determinato test posso impostare il mio SynchronizationContext.

Tieni presente che questo non è un metodo affidabile Determine if code is running as part of a unit testcome richiesto da OP poiché il codice potrebbe essere in esecuzione all'interno di un thread ma per alcuni scenari questa potrebbe essere una buona soluzione (anche: se sono già in esecuzione da un thread in background, potrebbe non essere necessario per iniziarne uno nuovo).


0

Application.Current è nullo quando viene eseguito con il tester di unità. Almeno per la mia app WPF che utilizza MS Unit tester. È un test facile da fare se necessario. Inoltre, qualcosa da tenere a mente quando si utilizza Application.Current nel codice.


0

Ho usato quanto segue in VB nel mio codice per verificare se siamo in uno unit test. in particolare non volevo che il test aprisse Word

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

0

Che ne dici di usare la riflessione e qualcosa del genere:

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

L'assembly chiamante sarà dove si trovano i tuoi casi di test e sostituirà semplicemente MainForm un tipo presente nel codice in fase di test.


-3

C'è anche una soluzione molto semplice quando stai testando una classe ...

Assegna semplicemente alla classe che stai testando una proprietà come questa:

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

Ora il tuo unit test può impostare il valore booleano "thisIsUnitTest" su true, quindi nel codice che vuoi saltare, aggiungi:

   if (thisIsUnitTest)
   {
       return;
   } 

È più facile e veloce che ispezionare i gruppi. Mi ricorda Ruby On Rails dove guarderesti per vedere se ti trovi nell'ambiente TEST.


1
Penso che tu sia stato screditato qui perché ti affidi al test stesso per modificare il comportamento della classe.
Riegardt Steyn

1
Questa non è peggiore di tutte le altre risposte qui.
DonO
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.