Определите, выполняется ли код как часть модульного теста

У меня есть модульный тест (nUnit). На многих уровнях вниз по стеку вызовов метод завершится ошибкой, если он запущен через модульный тест.

В идеале вы должны использовать что-то вроде насмешки для настройки объекта, от которого зависит этот метод, но это сторонний код, и я не могу сделать это без большой работы.

Я не хочу настраивать nUnit конкретные методы - здесь слишком много уровней и плохой способ выполнения юнит-тестирования.

Вместо этого я хотел бы добавить что-то вроде этого глубоко в стек вызовов

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

Так есть идеи о том, как написать IsRunningInUnitTest?

PS Я полностью осознаю, что это не очень хороший дизайн, но я думаю, что это лучше, чем альтернативы.

22 ответа

Решение

Я делал это раньше - я должен был держать нос, пока я делал это, но я сделал это. Прагматизм побеждает догматизм каждый раз. Конечно, если есть хороший способ избежать рефакторинга, это было бы здорово.

По сути, у меня был класс "UnitTestDetector", который проверял, была ли загружена сборка фреймворка NUnit в текущий домен приложений. Это нужно было сделать только один раз, а затем кешировать результат. Уродливый, но простой и эффективный.

Принимая идею Джона, это то, что я придумал -

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

Трубите сзади, мы все достаточно большие мальчики, чтобы понять, когда мы делаем то, что, вероятно, не должны;)

Адаптировано из ответа Райана. Это для платформы модульного тестирования MS.

Мне нужно это потому, что я показываю MessageBox на ошибках. Но мои модульные тесты также тестируют код обработки ошибок, и я не хочу, чтобы при запуске модульных тестов появлялся MessageBox.

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

И вот модульный тест для этого:

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

Упрощая решение Райана, вы можете просто добавить следующее статическое свойство в любой класс:

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

Я использую подобный подход, как Tallseth

Это основной код, который можно легко изменить, чтобы включить кеширование. Еще одна хорошая идея - добавить сеттер в IsRunningInUnitTest и позвонить UnitTestDetector.IsRunningInUnitTest = false к главной точке входа ваших проектов, чтобы избежать выполнения кода.

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

Может быть полезно проверить текущее имя процесса:

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

И эта функция также должна быть проверена с помощью unittest:

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

Рекомендации:
Мэтью Уотсон в http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/11e68468-c95e-4c43-b02b-7045a52b407e/

Где-то в тестируемом проекте:

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

Где-то в вашем проекте модульного тестирования:

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

Элегантно, нет. Но просто и быстро. AssemblyInitializer для MS Test. Я ожидаю, что другие тестовые структуры будут иметь эквиваленты.

В тестовом режиме Assembly.GetEntryAssembly() выглядит как нулевое.

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

Обратите внимание, что если Assembly.GetEntryAssembly() имеет значение null, Assembly.GetExecutingAssembly() - нет.

Документация гласит: метод GetEntryAssembly может возвратить ноль, когда управляемая сборка была загружена из неуправляемого приложения.

Просто используйте это:

AppDomain.CurrentDomain.IsDefaultAppDomain()

В тестовом режиме он вернет false.

Я использую это только для пропуска логики, которая отключает все TraceAppenders в log4net во время запуска, когда отладчик не подключен. Это позволяет модульным тестам регистрироваться в окне результатов Resharper даже при работе в режиме без отладки.

Метод, использующий эту функцию, вызывается либо при запуске приложения, либо при запуске тестового устройства.

Это похоже на сообщение Райана, но использует LINQ, отбрасывает требование System.Reflection, не кэширует результат и является частным для предотвращения (случайного) неправильного использования.

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

У меня есть решение, которое ближе к тому, что хотел исходный плакат. Проблема в том, как установить флаг теста, чтобы указать, что код выполняется как часть теста. Это может быть реализовано с помощью двух строк кода.

Я добавил внутреннюю переменную RunningNunitTest в начало класса. Обязательно сделайте это внутренней переменной, а не общедоступной. Мы не хотим экспортировать эту переменную при сборке проекта. Так же мы позволим NUnit установить значение true.

NUnit не имеет доступа к закрытым переменным или методам в нашем коде. Это простое решение. Между операторами using и пространством имен добавьте украшение [assembly: InternalsVisibleTo("NUnitTest")] . Это позволяет NUint получить доступ к любой внутренней переменной или методу. Мой тестовый проект NUnit называется «NUintTest». Замените это имя на имя вашего тестового проекта NUint.

Вот и все! Установите для RunningNunitTest значение true в ваших тестах NUnit.

      using NetworkDeviceScanner;

[assembly: InternalsVisibleTo("NUnitTest")] // Add this decoration to your class

namespace NetworkDeviceScannerLibrary
{
    public class DetectDevice
    {
        internal bool RunningNunitTest = false; // Add this variable to your class

        public ulong TotalAddressesFound;
        public ulong ScanCount;

Код NUnit

      var startIp = IPAddress.Parse("191.168.1.1");
var endIp = IPAddress.Parse("192.168.1.128");
var detectDevice = new DetectDevice
{
    RunningNunitTest = true
};
Assert.Throws<ArgumentOutOfRangeException>(() => detectDevice.DetectIpRange(startIp, endIp, null));

Наличие ссылки на nunit framework не означает, что тест на самом деле запущен. Например, в Unity при активации тестов режима воспроизведения ссылки на объекты добавляются в проект. И когда вы запускаете игру, ссылки существуют, поэтому UnitTestDetector не будет работать правильно.

Вместо того, чтобы проверять сборку nunit, мы можем попросить nunit api проверить, выполняется код сейчас или нет.

using NUnit.Framework;

// ...

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

Модульные тесты пропускают точку входа в приложение. По крайней мере, для wpf, winforms и консольного приложенияmain() не называется.

Если вызывается основной метод, то мы находимся во время выполнения, иначе мы находимся в режиме модульного тестирования:

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

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

Работает как шарм

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

,

Application.Current является нулевым при работе под модулем тестирования. По крайней мере, для моего приложения WPF, использующего MS Unit tester. Это простой тест, если нужно. Кроме того, что-то следует иметь в виду при использовании Application.Current в вашем коде.

Я был недоволен этой проблемой в последнее время. Я решил это немного по-другому. Во-первых, я не хотел делать предположение, что инфраструктура nunit никогда не будет загружена вне тестовой среды; Я был особенно обеспокоен разработчиками, запускающими приложение на своих машинах. Так что вместо этого я прошелся по стеку вызовов. Во-вторых, я смог предположить, что тестовый код никогда не будет запускаться на двоичных версиях выпуска, поэтому я убедился, что этот код не существует в системе выпуска.

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
        }

Существует также очень простое решение, когда вы тестируете класс...

Просто дайте классу, который вы тестируете, свойство вот так:

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

Теперь ваш модульный тест может установить для логического значения thisIsUnitTest значение true, поэтому в коде, который вы хотите пропустить, добавьте:

   if (thisIsUnitTest)
   {
       return;
   } 

Это проще и быстрее, чем проверка сборок. Напоминает мне о Ruby On Rails, где вы бы посмотрели, находитесь ли вы в среде TEST.

                  if (string.IsNullOrEmpty(System.Web.Hosting.HostingEnvironment.MapPath("~")))
            {
                // Running not as a web app (unit tests)
            }

            // Running as a web app

Учитывая, что ваш код обычно запускается в основном потоке (GUI) приложения Windows Form, и вы хотите, чтобы он вел себя по-другому во время работы в тесте, который вы можете проверить

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

Я использую это для кода, который я хочу fire and forgot в приложении с графическим интерфейсом, но в модульных тестах мне может понадобиться вычисленный результат для диссертации, и я не хочу связываться с несколькими выполняющимися потоками.

Работает на МСТест. Преимущество в том, что мой код не должен проверять саму структуру тестирования, и если мне действительно нужно асинхронное поведение в определенном тесте, я могу установить свой собственный SynchronizationContext.

Имейте в виду, что это не надежный метод Determine if code is running as part of a unit test в соответствии с запросом OP, поскольку код может выполняться внутри потока, но для некоторых сценариев это может быть хорошим решением (также: если я уже работаю из фонового потока, возможно, нет необходимости запускать новый).

Как насчет использования отражения и чего-то вроде этого:

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

Вызывающая сборка будет там, где находятся ваши тестовые примеры, и просто замените MainForm некоторым типом, который в вашем тестируемом коде.

Я использовал следующее в VB в своем коде, чтобы проверить, участвуем ли мы в модульном тесте. spifically я не хотел, чтобы тест открыл Word

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

Вы можете использовать Splat для выполнения, а также многих других кроссплатформенных задач.

Пример:

      // If true, we are running unit tests
ModeDetector.InUnitTestRunner();
Другие вопросы по тегам