【问题标题】:Determine if code is running as part of a unit test确定代码是否作为单元测试的一部分运行
【发布时间】:2011-03-11 04:54:15
【问题描述】:

我有一个单元测试 (nUnit)。如果方法通过单元测试运行,调用堆栈中的许多层都会失败。

理想情况下,您会使用模拟之类的东西来设置此方法所依赖的对象,但这是第 3 方代码,如果没有大量工作,我无法做到这一点。

我不想设置特定于 nUnit 的方法 - 这里的级别太多,而且它是一种糟糕的单元测试方式。

相反,我想做的是在调用堆栈的深处添加类似这样的内容

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

那么关于如何编写 IsRunningInUnitTest 有什么想法吗?

附:我完全意识到这不是很好的设计,但我认为它比其他选择更好。

【问题讨论】:

  • 您不应在单元测试中直接或间接测试第三方代码。您应该将您的测试方法与第三方实现隔离开来。
  • 是的 - 我意识到 - 在一个想法世界中,但有时我们必须对事情有点务实,不是吗?
  • 回到克雷格的评论 - 不确定那是真的。如果我的方法依赖于以某种方式表现的 3rd 方库,那么这不应该成为测试的一部分吗?如果第 3 方应用程序发生更改,我希望我的测试失败。如果您使用模拟您的测试是针对您认为 3rd 方应用程序的工作方式,而不是它的实际工作方式。
  • Ryan,您可以测试有关第三方行为的假设,但这是一个单独的测试。您需要单独测试自己的代码。
  • 我确实明白你的意思,但除了一个微不足道的例子之外,你会谈论大量(大量)的工作,并且没有什么可以确保你在测试中检查的假设是与您在实际方法中的假设相同。嗯 - 我想为一篇博文进行辩论,当我把我的想法集中在一起时,我会给你发一封电子邮件。

标签: c# reflection nunit


【解决方案1】:

我以前做过这个 - 我做的时候必须捏住鼻子,但我做到了。实用主义每次都战胜教条主义。当然,如果一个很好的方法可以重构来避免它,那就太好了。

基本上,我有一个“UnitTestDetector”类,它检查 NUnit 框架程序集是否已加载到当前 AppDomain 中。它只需要这样做一次,然后缓存结果。丑陋,但简单而有效。

【讨论】:

  • 有任何关于 UnitTestDetector 的样本吗?和 MSTest 类似?
  • @Kiquenet:我想我会使用AppDomain.GetAssemblies 并检查相关程序集 - 对于 MSTest,您需要查看加载了哪些程序集。以 Ryan 的回答为例。
  • 这对我来说不是一个好方法。我正在从控制台应用程序调用 UnitTest 方法,它认为它是一个 UnitTest 应用程序。
  • @Bizhan:我建议你当时处于一个相当专业的情况,你不应该期望更一般的答案会起作用。您可能想针对您的所有具体要求提出一个新问题。 (例如,“从控制台应用程序调用您的代码”和“测试运行程序”之间有什么区别?您希望如何区分您的控制台应用程序和任何其他基于控制台的测试运行程序?)
  • @Kiquenet "Microsoft.VisualStudio.TestPlatform.MSTestAdapter"
【解决方案2】:

接受 Jon 的想法,这就是我想出的 -

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

我们都大到足以认出我们什么时候做了一些我们可能不应该做的事情;)

【讨论】:

  • +1 好答案。不过,这可以简化很多,见下文:stackoverflow.com/a/30356080/184528
  • 我写这个的特定项目是(现在仍然是!).NET 2.0 所以没有 linq。
  • 这对我有用,但似乎程序集名称已经改变了。我切换到Kiquenet's solution
  • 我不得不关闭 travis ci 构建的日志记录,它冻结了一切
  • 对我有用,我必须用仅在单元测试中发生的剃刀解决 .NET core 3 错误。
【解决方案3】:

改编自 Ryan 的回答。这个是针对 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
    }

【讨论】:

  • 我有一个更好的方法可以解决您的 MessageBox 问题,并避免这种 hack 并提供更多的单元测试用例。我使用了一个实现我称为 ICommonDialogs 的接口的类。实现类显示所有弹出对话框(消息框、文件对话框、颜色选择器、数据库连接对话框等)。需要显示消息框的类接受 ICommonDiaglogs 作为构造函数参数,然后我们可以在单元测试中对其进行模拟。奖励:您可以断言预期的 MessageBox 调用。
  • @Tony,好主意。这显然是最好的方法。我不知道我当时在想什么。我认为依赖注入当时对我来说还是个新鲜事物。
  • 说真的,人们,学习依赖注入,其次,模拟对象。依赖注入将彻底改变您的编程。
  • 我会将 UnitTestDetector.IsInUnitTest 实现为“return true”,您的单元测试将通过。 ;) 似乎不可能进行单元测试的有趣事情之一。
  • Microsoft.VisualStudio.QualityTools.UnitTestFramework 不再适合我了。将其更改为 Microsoft.VisualStudio.TestPlatform.TestFramework - 再次有效。
【解决方案4】:

简化 Ryan 的解决方案,您只需将以下静态属性添加到任何类:

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

【讨论】:

  • 与 dan-gph 的答案几乎相同(尽管那是在寻找 VS 工具集,而不是 nunit)。
  • 也可以与 xunit 一起使用,只需将 StartsWith 部分替换为“xunit.runner”
【解决方案5】:

我使用与 tallseth 类似的方法

这是可以轻松修改以包含缓存的基本代码。 另一个好主意是向IsRunningInUnitTest 添加一个setter 并将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# 测试运行程序)。
  • 这种方法可行,但每次调用 IsRunningInUnitTest getter 时都会查找这些属性。在某些情况下,它可能会影响性能。检查 AssemblyName 更便宜,因为它只进行一次。公共设置器的想法很好,但在这种情况下,UnitTestDetector 类应该放在共享程序集中。
  • FWIW:NUnit 可以运行没有此类属性的测试(旧版测试检测)。当然,在这种情况下更新测试用例可能是最简单的..
  • 这将在运行于不同线程上下文的代码中失败,甚至可能在静态类型初始化中(取决于用法)。
  • 就性能而言:缓存变量会解决这个问题,因为通常可以安全地假设,如果在测试中运行一次,它会在任何地方运行测试......这反过来会隐藏问题#2。它也可以与装配检查结合使用。
【解决方案6】:

也许有用,检查当前的 ProcessName:

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/

【讨论】:

  • || processName.StartsWith("testhost") // testhost.x86 用于 VS 2019
  • 这在 ReSharper 测试中失败(“testhost”也是如此)。这些支票都太脆弱了。
  • @user2864740 你有 ReSharper 测试的解决方案吗? ReSharper 是第三方,不是免费的,不包含在 Visual Studio 中。
  • 我只是指出它并不能在所有测试运行器上运行。可悲的是,虽然 VS 测试运行器和 dotnet 测试运行器将执行程序集名称设置为“testhost”,但这并不是通用的(我不确定 XUnit 或 MBUnit .. 如果有人仍然使用它们)。我们采用的解决方案进行了多项检查:执行程序集名称、进程名称,然后检查 NUnit 引用(以及调用堆栈中的夹具属性,类似于此处其他答案所示)。它仍然很脆弱,因为所有这些都是特定测试运行器/框架的工件,以后可能需要更新。 :-/
【解决方案7】:

在测试模式下,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() 不是。

documentation 表示:当从非托管应用程序加载托管程序集时,GetEntryAssembly 方法可以返回 null

【讨论】:

  • 自从引入 dotnetcore 后,这种行为发生了变化。即使在 2012 年,这也会在 IIS 等环境中报告误报。
【解决方案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 测试。我希望其他测试框架也有等价物。

【讨论】:

  • 如果您正在测试的代码创建了额外的 AppDomain,IsRunningInUnitTest 不会在这些 AppDomain 中设置为 true。
  • 但可以通过添加共享程序集或在每个域中声明 IsRunningInUnitTest 轻松解决。
  • 对于 NUnit 3+,合适的位置可能是 OneTimeSetUp。
【解决方案9】:

就用这个吧:

AppDomain.CurrentDomain.IsDefaultAppDomain()

在测试模式下,它会返回false。

【讨论】:

【解决方案10】:

我使用这个only 来跳过逻辑,在启动期间在没有附加调试器的情况下禁用 log4net 中的所有 TraceAppenders。即使在非调试模式下运行,这也允许单元测试记录到 Resharper 结果窗口。

使用此函数的方法要么在应用程序启动时调用,要么在开始测试夹具时调用。

它类似于 Ryan 的帖子,但使用 LINQ,放弃了 System.Reflection 要求,不缓存结果,并且是私有的以防止(意外)滥用。

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

【讨论】:

    【解决方案11】:

    引用 nunit 框架并不意味着测试实际上正在运行。例如,在 Unity 中,当您激活播放模式测试时,会将 nunit 引用添加到项目中。当你运行游戏时,引用是存在的,所以 UnitTestDetector 将无法正常工作。

    我们可以要求 nunit api 检查是否正在执行测试的代码,而不是检查 nunit 程序集。

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

    编辑:

    如果需要,请注意TestContext may be automatically generated

    【讨论】:

    • 这需要在“可能的非测试代码”中明确引用 NUnit。如果程序集被加载,也许使用 NUnit 程序集检查和反射来测试上下文。
    【解决方案12】:

    单元测试将跳过应用程序入口点。至少对于 wpf,winforms 和控制台应用程序main() 没有被调用。

    如果 main 方法被调用而不是我们在 run-time,否则我们在 unit test 模式:

    public static bool IsUnitTest { get; private set; } = true;
    
    [STAThread]
    public static void main()
    {
        IsUnitTest = false;
        ...
    }
    

    【讨论】:

      【解决方案13】:

      我最近很不高兴遇到这个问题。我以稍微不同的方式解决了它。首先,我不愿意假设 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
              }
      

      【讨论】:

      • 我发现在发布发布版本的同时测试调试版本的想法通常是一个坏主意。
      【解决方案14】:

      像魅力一样工作

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

      .

      【讨论】:

        【解决方案15】:

        Application.Current 在单元测试器下运行时为空。至少对于我使用 MS 单元测试器的 WPF 应用程序而言。如果需要,这是一个简单的测试。此外,在您的代码中使用 Application.Current 时要记住一些事情。

        【讨论】:

          【解决方案16】:

          我有一个更接近原始发帖人想要的解决方案。问题是如何设置测试标志以指示代码正在作为测试的一部分执行。这可以用 2 行代码来实现。

          我在类的顶部添加了一个名为 RunningNunitTest 的内部变量。确保将其设为内部变量而不是公开的。我们不想在构建项目时导出这个变量。这也是我们允许 NUnit 将其设置为 true 的方式。

          NUnit 无法访问我们代码中的私有变量或方法。这是一个简单的修复。在 using 语句和命名空间之间添加一个 [assembly: InternalsVisibleTo("NUnitTest")] 装饰。这允许 NUint 访问任何内部变量或方法。我的 NUnit 测试项目名为“NUintTest”。将此名称替换为您的 NUint 测试项目的名称。

          就是这样!在 NUnit 测试中将 RunningNunitTest 设置为 true。

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

          【讨论】:

            【解决方案17】:
                        if (string.IsNullOrEmpty(System.Web.Hosting.HostingEnvironment.MapPath("~")))
                        {
                            // Running not as a web app (unit tests)
                        }
            
                        // Running as a web app
            

            【讨论】:

            • 这是如何工作的?感觉就像您依赖于一些无证行为。
            • docs.microsoft.com/en-us/dotnet/api/… 它说:返回由 virtualPath 指定的 服务器 上的物理路径。对于控制台应用程序、单元测试甚至桌面应用程序,根本没有服务器
            【解决方案18】:

            考虑到您的代码通常在 Windows 窗体应用程序的主 (gui) 线程中运行,并且您希望它在测试中运行时表现不同,您可以检查

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

            我将它用于我想在 gui 应用程序中 fire and forgot 的代码,但在单元测试中我可能需要计算结果来进行断言,并且我不想弄乱多个正在运行的线程。

            适用于 MSTest。优点是我的代码不需要检查测试框架本身,如果我真的需要某个测试中的异步行为,我可以设置自己的 SynchronizationContext。

            请注意,这不是 OP 要求的 Determine if code is running as part of a unit test 的可靠方法,因为代码可以在线程内运行,但对于某些情况,这可能是一个很好的解决方案(另外:如果我已经从后台线程运行,可能没有必要重新开始)。

            【讨论】:

              【解决方案19】:

              我在 VB 中的代码中使用了以下内容来检查我们是否在单元测试中使用 ae。特别是我不想让测试打开 Word

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

              【讨论】:

                【解决方案20】:

                如何使用反射和类似的东西:

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

                调用程序集将是您的测试用例所在的位置,并且只是替换 MainForm 正在测试的代码中的某种类型。

                【讨论】:

                • 但是你不能期望调用者总是测试程序集。调用程序集的调用者可以是单元测试程序集。很难说
                【解决方案21】:

                当你测试一个类时,还有一个非常简单的解决方案......

                只需给你正在测试的类一个这样的属性:

                // 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 环境中。

                【讨论】:

                • 我认为您在这里被否决了,因为您依靠测试本身来修改班级的行为。
                • 这并不比这里的所有其他答案最糟糕。
                猜你喜欢
                • 1970-01-01
                • 2011-04-06
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-05-21
                相关资源
                最近更新 更多