Determine si el código se está ejecutando como parte de una prueba unitaria

105

Tengo una prueba unitaria (nUnit). En muchas capas de la pila de llamadas, un método fallará si se ejecuta mediante una prueba unitaria.

Idealmente, usaría algo como burlarse para configurar el objeto del que depende este método, pero este es un código de terceros y no puedo hacerlo sin mucho trabajo.

No quiero configurar métodos específicos de nUnit: hay demasiados niveles aquí y es una mala manera de hacer la prueba unitaria.

En cambio, lo que me gustaría hacer es agregar algo como esto en el fondo de la pila de llamadas

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

Entonces, ¿alguna idea sobre cómo escribir IsRunningInUnitTest?

PD: Soy plenamente consciente de que este no es un gran diseño, pero creo que es mejor que las alternativas.

Ryan
fuente
5
No debe probar directa o indirectamente código de terceros en una prueba unitaria. Debe aislar su método bajo prueba de la implementación de terceros.
Craig Stuntz
14
Sí, me doy cuenta de eso, en un mundo de ideas, pero a veces tenemos que ser un poco pragmáticos sobre las cosas, ¿no?
Ryan
9
Volviendo al comentario de Craig, no estoy seguro de que sea cierto. Si mi método se basa en que la biblioteca de terceros se comporte de cierta manera, ¿no debería ser esto parte de la prueba? Si la aplicación de terceros cambia, quiero que mi prueba falle. Si está usando simulacros de sus pruebas contra cómo cree que funciona la aplicación de terceros, no cómo lo hace realmente.
Ryan
2
Ryan, puedes probar suposiciones sobre el comportamiento de terceros, pero esa es una prueba separada. Necesita probar su propio código de forma aislada.
Craig Stuntz
2
Entiendo lo que dice, pero para cualquier cosa menos un ejemplo trivial, estaría hablando de una gran (enorme) cantidad de trabajo y no hay nada que garantice que las suposiciones que verifica en su prueba sean las mismas que las suposiciones en sus métodos reales. . Mmmm - debate para una publicación de blog, creo, te enviaré un correo electrónico cuando tenga mis pensamientos juntos.
Ryan

Respuestas:

80

He hecho esto antes, tuve que taparme la nariz mientras lo hacía, pero lo hice. El pragmatismo siempre gana al dogmatismo. Por supuesto, si no es una buena manera se puede refactorizar para evitarlo, que sería grande.

Básicamente, tenía una clase "UnitTestDetector" que verificaba si el ensamblaje del marco NUnit estaba cargado en el AppDomain actual. Solo necesitaba hacer esto una vez, luego almacenar en caché el resultado. Feo, pero sencillo y eficaz.

Jon Skeet
fuente
alguna muestra sobre UnitTestDetector? y similar para MSTest?
Kiquenet
4
@Kiquenet: Creo que solo usaría AppDomain.GetAssembliesy verificaría el ensamblaje relevante; para MSTest, necesitaría ver qué ensamblajes están cargados. Mira la respuesta de Ryan para ver un ejemplo.
Jon Skeet
Este no es un buen enfoque para mí. Estoy llamando a un método UnitTest desde una aplicación de consola y cree que es una aplicación UnitTest.
Bizhan
1
@Bizhan: Le sugiero que se encuentre en una situación bastante especializada, y no debería esperar que funcionen respuestas más generales. Es posible que desee hacer una nueva pregunta con todos sus requisitos específicos. (¿Cuál es la diferencia entre "el código que llama desde una aplicación de consola" y "un ejecutor de prueba", por ejemplo? ¿Cómo le gustaría distinguir entre su aplicación de consola y cualquier otro ejecutor de prueba basado en consola?)
Jon Skeet
74

Tomando la idea de Jon, esto es lo que se me ocurrió:

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

Silencio desde atrás, todos somos chicos lo suficientemente grandes como para reconocer cuando estamos haciendo algo que probablemente no deberíamos hacer;)

Ryan
fuente
2
+1 Buena respuesta. Sin embargo, esto se puede simplificar bastante, consulte a continuación: stackoverflow.com/a/30356080/184528
cdiggins
El proyecto en particular para el que escribí esto era (¡y sigue siendo!) .NET 2.0, así que no hay linq.
Ryan
Esto solía funcionar para mí, pero parece que el nombre del ensamblado ha cambiado desde entonces. Me cambié a la solución de Kiquenet
The_Black_Smurf
Tuve que desactivar el registro para las compilaciones de travis ci, congeló todo
jjxtra
Funciona para mí, tengo que hackear los errores de .NET core 3 con maquinilla de afeitar que solo ocurren en las pruebas unitarias.
jjxtra
62

Adaptado de la respuesta de Ryan. Éste es para el marco de pruebas unitarias de MS.

La razón por la que necesito esto es porque muestro un cuadro de mensaje en los errores. Pero mis pruebas unitarias también prueban el código de manejo de errores, y no quiero que aparezca un MessageBox al ejecutar las pruebas unitarias.

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

Y aquí hay una prueba unitaria para ello:

    [TestMethod]
    public void IsInUnitTest()
    {
        Assert.IsTrue(UnitTestDetector.IsInUnitTest, 
            "Should detect that we are running inside a unit test."); // lol
    }
dan-gph
fuente
8
Tengo una mejor manera que resuelve su problema de MessageBox y anula este truco y ofrece más casos de prueba unitaria. Utilizo una clase que implementa una interfaz que llamo ICommonDialogs. La clase de implementación muestra todos los cuadros de diálogo emergentes (cuadro de mensaje, cuadros de diálogo de archivo, selector de color, cuadro de diálogo de conexión de base de datos, etc.). Las clases que necesitan mostrar cuadros de mensaje aceptan ICommonDiaglogs como un parámetro de constructor que luego podemos simular en la prueba unitaria. Bonificación: puede hacer valer las llamadas de MessageBox esperadas.
Tony O'Hagan
1
@ Tony, buena idea. Esa es claramente la mejor manera de hacerlo. No sé qué estaba pensando en ese momento. Creo que la inyección de dependencia todavía era nueva para mí en ese momento.
dan-gph
3
En serio, gente, aprenda sobre la inyección de dependencia y, en segundo lugar, simule objetos. La inyección de dependencia revolucionará su programación.
dan-gph
2
Implementaré UnitTestDetector.IsInUnitTest como "return true" y su prueba unitaria pasará. ;) Una de esas cosas divertidas que parece imposible de probar unitariamente.
Samer Adra
1
Microsoft.VisualStudio.QualityTools.UnitTestFramework ya no me funcionó. Lo cambió a Microsoft.VisualStudio.TestPlatform.TestFramework, que funciona de nuevo.
Alexander
21

Simplificando la solución de Ryan, puede agregar la siguiente propiedad estática a cualquier clase:

    public static readonly bool IsRunningFromNUnit = 
        AppDomain.CurrentDomain.GetAssemblies().Any(
            a => a.FullName.ToLowerInvariant().StartsWith("nunit.framework"));
cdiggins
fuente
2
Casi lo mismo que la respuesta de dan-gph (aunque buscaba el conjunto de herramientas VS, no nunit).
Ryan
18

Utilizo un enfoque similar al de tallseth

Este es el código básico que podría modificarse fácilmente para incluir el almacenamiento en caché. Otra buena idea sería agregar un establecedor IsRunningInUnitTesty llamar UnitTestDetector.IsRunningInUnitTest = falseal punto de entrada principal de su proyecto para evitar la ejecución del código.

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
fuente
Me gusta más este enfoque que las respuestas más votadas. No creo que sea seguro asumir que los ensamblajes de prueba unitaria solo se cargarán durante una prueba unitaria y que el nombre del proceso también puede variar de un desarrollador a otro (por ejemplo, algunos usan el ejecutor de pruebas R #).
EM0
Este enfoque funcionaría, pero buscará esos atributos cada vez que se llame al getter IsRunningInUnitTest . Puede haber algunos casos en los que pueda afectar el rendimiento. Verificar el AssemblyName es más barato porque se hace solo una vez. La idea con el setter público es buena, pero en este caso, la clase UnitTestDetector debe colocarse en un ensamblado compartido.
Sergey
13

Quizás útil, verificando ProcessName actual:

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

Y esta función también debe ser verificada por unittest:

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

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

Kiquenet
fuente
|| processName.StartsWith("testhost") // testhost.x86para VS 2019
Kiquenet
9

En modo de prueba, Assembly.GetEntryAssembly()parece ser null.

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

Tenga en cuenta que si lo Assembly.GetEntryAssembly()es null, Assembly.GetExecutingAssembly()no lo es.

La documentación dice: El GetEntryAssemblymétodo puede regresar nullcuando se ha cargado un ensamblado administrado desde una aplicación no administrada.

Eric Bole-Feysot
fuente
8

En algún lugar del proyecto que se está probando:

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

En algún lugar de su proyecto de prueba unitaria:

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

Elegante, no. Pero sencillo y rápido. AssemblyInitializeres para MS Test. Esperaría que otros marcos de prueba tuvieran equivalentes.

Edward Brey
fuente
1
Si el código que está probando crea AppDomains adicionales, IsRunningInUnitTestno se establece como verdadero en esos AppDomains.
Edward Brey
Pero podría resolverse fácilmente agregando un ensamblado compartido o declarando IsRunningInUnitTest en cada dominio.
Sergey
3

Utilizo esto solo para omitir la lógica que deshabilita todos los TraceAppenders en log4net durante el inicio cuando no hay un depurador adjunto. Esto permite que las pruebas unitarias se registren en la ventana de resultados de Resharper incluso cuando se ejecutan en modo sin depuración.

El método que utiliza esta función se llama al inicio de la aplicación o al iniciar un dispositivo de prueba.

Es similar a la publicación de Ryan pero usa LINQ, elimina el requisito System.Reflection, no almacena en caché el resultado y es privado para evitar un uso indebido (accidental).

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

Tener una referencia al marco nunit no significa que la prueba se esté ejecutando. Por ejemplo, en Unity, cuando activa las pruebas del modo de reproducción, las referencias de nunit se agregan al proyecto. Y cuando ejecutas un juego, las referencias existen, por lo que UnitTestDetector no funcionaría correctamente.

En lugar de verificar el ensamblaje de nunit, podemos pedirle a la api de nunit que verifique si el código se está ejecutando ahora o no.

using NUnit.Framework;

// ...

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

Editar:

Tenga en cuenta que TestContext se puede generar automáticamente si es necesario.

ChessMax
fuente
2
Por favor, no descargue el código aquí. Explique lo que hace.
nkr
2

Solo usa esto:

AppDomain.CurrentDomain.IsDefaultAppDomain()

En el modo de prueba, devolverá falso.

shinexyt
fuente
1

No estaba contento de tener este problema recientemente. Lo resolví de una manera ligeramente diferente. Primero, no estaba dispuesto a asumir que el marco nunit nunca se cargaría fuera de un entorno de prueba; Estaba particularmente preocupado por los desarrolladores que ejecutaban la aplicación en sus máquinas. En su lugar, recorrí la pila de llamadas. En segundo lugar, pude suponer que el código de prueba nunca se ejecutaría en binarios de lanzamiento, así que me aseguré de que este código no existiera en un sistema de lanzamiento.

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
fuente
Me parece que la idea de probar la versión de depuración mientras se envía la versión de lanzamiento es una mala idea en general.
Patrick M
1

Funciona de maravilla

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 noble
fuente
1

Las pruebas unitarias omitirán el punto de entrada de la aplicación. Al menos para wpf, main()no se llama a winforms ni a la aplicación de consola .

Si se llama al método principal, entonces estamos en tiempo de ejecución ; de lo contrario, estamos en modo de prueba unitaria :

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

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

Teniendo en cuenta que su código se ejecuta normalmente en el hilo principal (gui) de una aplicación de formularios de Windows y desea que se comporte de manera diferente mientras se ejecuta en una prueba que puede verificar

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

Estoy usando esto para el código que quiero fire and forgoten una aplicación de interfaz gráfica de usuario, pero en las pruebas unitarias podría necesitar el resultado calculado para una afirmación y no quiero meterme con varios subprocesos en ejecución.

Funciona para MSTest. La ventaja es que mi código no necesita verificar el marco de prueba en sí y si realmente necesito el comportamiento asíncrono en una prueba determinada, puedo configurar mi propio SynchronizationContext.

Tenga en cuenta que este no es un método confiable para Determine if code is running as part of a unit testlo solicitado por OP, ya que el código podría estar ejecutándose dentro de un hilo, pero para ciertos escenarios, esta podría ser una buena solución (también: si ya estoy ejecutando desde un hilo en segundo plano, puede que no sea necesario para iniciar uno nuevo).

Jürgen Steinblock
fuente
0

Application.Current es nulo cuando se ejecuta bajo el probador de la unidad. Al menos para mi aplicación WPF que usa MS Unit tester. Es una prueba fácil de hacer si es necesario. Además, algo a tener en cuenta al usar Application.Current en su código.

Glaucus
fuente
0

He usado lo siguiente en VB en mi código para verificar si estamos en una prueba unitaria. específicamente, no quería que la prueba abriera Word

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

¿Qué tal usar la reflexión y algo como esto?

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

El ensamblado de llamada estará donde están sus casos de prueba y simplemente sustituirá a MainForm por algún tipo que esté en su código que se está probando.

Jon
fuente
-3

También hay una solución realmente simple cuando estás probando una clase ...

Simplemente dé a la clase que está probando una propiedad como esta:

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

Ahora su prueba unitaria puede establecer el booleano "thisIsUnitTest" en verdadero, por lo que en el código que desea omitir, agregue:

   if (thisIsUnitTest)
   {
       return;
   } 

Es más fácil y rápido que inspeccionar los ensamblajes. Me recuerda a Ruby On Rails, donde mirarías para ver si estás en el entorno TEST.

Danmar Herholdt
fuente
1
Creo que te votaron en contra porque confías en la prueba en sí para modificar el comportamiento de la clase.
Riegardt Steyn
1
Esta no es peor que todas las otras respuestas aquí.
DonO