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

107

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

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

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

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

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

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

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

Райан
источник
5
Вы не должны прямо или косвенно тестировать сторонний код в модульном тесте. Вы должны изолировать тестируемый метод от сторонней реализации.
Craig Stuntz
15
Да - я понимаю это - в мире идей, но иногда нам нужно быть немного прагматичными в отношении вещей, нет?
Райан
9
Возвращаясь к комментарию Крейга - не уверен, что это правда. Если мой метод полагается на поведение сторонней библиотеки определенным образом, разве это не должно быть частью теста? Если стороннее приложение изменится, я хочу, чтобы мой тест не прошел. Если вы используете имитацию своего тестирования против того, как, по вашему мнению, работает стороннее приложение, а не того, как оно работает на самом деле.
Ryan
2
Райан, вы можете проверить предположения о поведении третьей стороны, но это отдельный тест. Вам нужно изолированно протестировать свой собственный код.
Craig Stuntz
2
Я понимаю, что вы говорите, но для чего угодно, кроме тривиального примера, вы бы говорили о большом (огромном) объеме работы, и нет ничего, что могло бы гарантировать, что предположения, которые вы проверяете в вашем тесте, совпадают с вашими предположениями в ваших реальных методах . Хм - обсуждение поста в блоге Я думаю, я напишу вам электронное письмо, когда соберусь с мыслями.
Ryan

Ответы:

80

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

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

Джон Скит
источник
любой образец о UnitTestDetector? а подобное для MSTest?
Kiquenet 05
4
@Kiquenet: я бы просто использовал AppDomain.GetAssembliesи проверял соответствующую сборку - для MSTest вам нужно будет посмотреть, какие сборки загружены. Взгляните на ответ Райана для примера.
Джон Скит
Для меня это не лучший подход. Я вызываю метод UnitTest из консольного приложения, и он думает, что это приложение UnitTest.
Бижан
1
@Bizhan: Я бы посоветовал вам тогда оказаться в довольно специфической ситуации, и не стоит ожидать, что более общие ответы сработают. Возможно, вы захотите задать новый вопрос со всеми вашими конкретными требованиями. (В чем разница между «вызовом вашего кода из консольного приложения» и, например, «средством выполнения тестов»? Как бы вы хотели отличать свое консольное приложение от любого другого средства выполнения тестов на основе консоли?)
Джон Скит,
75

Взяв идею Джона, я пришел к выводу:

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

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

Райан
источник
2
+1 Хороший ответ. Это можно немного упростить, см. Ниже: stackoverflow.com/a/30356080/184528
cdiggins
Конкретный проект, для которого я написал это, был (и остается!) .NET 2.0, поэтому без linq.
Райан
Это работает для меня, но похоже, что с тех пор имя сборки изменилось. Я переключился на решение Kiquenet в
The_Black_Smurf
Мне пришлось отключить ведение журнала для сборок travis ci, все заморозило
jjxtra
У меня работает, я должен с помощью бритвы устранять ошибки .NET core 3, которые возникают только в модульных тестах.
jjxtra
62

Адаптировано из ответа Райана. Это для среды модульного тестирования 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
    }
дан-гф
источник
8
У меня есть лучший способ решить вашу проблему с MessageBox, аннулировать этот взлом и предоставить больше примеров модульного тестирования. Я использую класс, реализующий интерфейс, который я называю ICommonDialogs. Класс реализации отображает все всплывающие диалоговые окна (MessageBox, диалоги файлов, выбор цвета, диалог подключения к базе данных и т. Д.). Классы, которым необходимо отображать окна сообщений, принимают ICommonDiaglogs в качестве параметра конструктора, который мы затем можем имитировать в модульном тесте. Бонус: вы можете утверждать ожидаемые вызовы MessageBox.
Тони О'Хаган
1
@ Тони, хорошая идея. Очевидно, это лучший способ сделать это. Не знаю, о чем я думал в то время. Я думаю, что в то время внедрение зависимостей было для меня в новинку.
dan-gph
3
Серьезно, люди узнают о внедрении зависимостей и, во-вторых, имитируют объекты. Внедрение зависимостей произведет революцию в вашем программировании.
dan-gph
2
Я буду реализовывать UnitTestDetector.IsInUnitTest как «return true», и ваш модульный тест пройдет. ;) Одна из тех забавных вещей, которая кажется невозможной для модульного тестирования.
Самер Адра
1
Microsoft.VisualStudio.QualityTools.UnitTestFramework у меня больше не работал. Изменил его на Microsoft.VisualStudio.TestPlatform.TestFramework - это снова работает.
Александр
22

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

    public static readonly bool IsRunningFromNUnit = 
        AppDomain.CurrentDomain.GetAssemblies().Any(
            a => a.FullName.ToLowerInvariant().StartsWith("nunit.framework"));
cdiggins
источник
2
Практически то же самое, что и ответ dan-gph (хотя он искал набор инструментов VS, а не nunit).
Райан
19

Я использую тот же подход, что и таллсет

Это базовый код, который можно легко изменить, включив кеширование. Еще одна хорошая идея - добавить сеттер 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;
        }
    }
}
Юрген Штайнблок
источник
Мне этот подход нравится больше, чем ответы с большим количеством голосов. Я не думаю, что безопасно предполагать, что сборки модульного теста будут загружаться только во время модульного теста, и имя процесса также может варьироваться от разработчика к разработчику (например, некоторые используют средство запуска тестов R #).
EM0 02
Этот подход будет работать, но он будет искать эти атрибуты каждый раз, когда вызывается метод получения IsRunningInUnitTest . В некоторых случаях это может повлиять на производительность. Проверка AssemblyName дешевле, поскольку выполняется только один раз. Идея с общедоступным сеттером хороша, но в этом случае класс UnitTestDetector следует поместить в общую сборку.
Сергей
13

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

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/

Kiquenet
источник
|| processName.StartsWith("testhost") // testhost.x86для VS 2019
Kiquenet
9

В тестовом режиме Assembly.GetEntryAssembly()вроде бы есть null.

#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метод может возвращаться, nullкогда управляемая сборка загружена из неуправляемого приложения.

Эрик Боле-Фейсот
источник
8

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

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. Я ожидал бы, что у других тестовых фреймворков будут эквиваленты.

Эдвард Брей
источник
1
Если код, который вы тестируете, создает дополнительные домены приложений, IsRunningInUnitTestдля этих доменов приложений не устанавливается значение true.
Эдвард Брей
Но это можно легко решить, добавив общую сборку или объявив IsRunningInUnitTest в каждом домене.
Сергей
3

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

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

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

    private static bool IsNUnitRunning()
    {
        return AppDomain.CurrentDomain.GetAssemblies().Any(assembly => assembly.FullName.ToLowerInvariant().StartsWith("nunit.framework"));
    }
Грэм Викстед
источник
3

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

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

using NUnit.Framework;

// ...

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

Редактировать:

Помните, что TestContext может быть автоматически сгенерирован, если это необходимо.

ChessMax
источник
2
Пожалуйста, не просто сбрасывайте сюда код. Объясните, что он делает.
nkr
3

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

AppDomain.CurrentDomain.IsDefaultAppDomain()

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

shinexyt
источник
1

Я был недоволен этой проблемой недавно. Я решил это немного по-другому. Во-первых, я не хотел делать предположение, что фреймворк 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
        }
Таллсет
источник
Я считаю, что идея тестирования отладочной версии при отправке релизной версии в целом является плохой.
Patrick M
1

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

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

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

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

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

[STAThread]
public static void main()
{
    IsUnitTest = false;
    ...
}
Синатр
источник
0

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

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в приложении с графическим интерфейсом, но в модульных тестах мне может потребоваться вычисленный результат для диссертации, и я не хочу связываться с несколькими запущенными потоками.

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

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

Юрген Штайнблок
источник
0

Application.Current имеет значение NULL при работе под модулем тестера. По крайней мере, для моего приложения WPF с использованием тестера MS Unit. При необходимости это легко сделать. Кроме того, о чем следует помнить при использовании Application.Current в вашем коде.

Главкус
источник
0

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

    If Not Application.ProductName.ToLower().Contains("test") then
        ' Do something 
    End If
TomShantisoft
источник
0

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

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

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

Джон
источник
-3

Когда вы тестируете класс, есть действительно простое решение ...

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

// 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.

Данмар Херхольдт
источник
1
Я думаю, вы здесь не проголосовали, потому что вы полагаетесь на сам тест, чтобы изменить поведение класса.
Riegardt Steyn
1
Это не хуже, чем все остальные ответы здесь.
DonO