ChatGPT解决这个技术问题 Extra ChatGPT

在 C# 中对私有方法进行单元测试

Visual Studio 允许通过自动生成的访问器类对私有方法进行单元测试。我已经编写了一个成功编译的私有方法的测试,但它在运行时失败。代码和测试的一个相当小的版本是:

//in project MyProj
class TypeA
{
    private List<TypeB> myList = new List<TypeB>();

    private class TypeB
    {
        public TypeB()
        {
        }
    }

    public TypeA()
    {
    }

    private void MyFunc()
    {
        //processing of myList that changes state of instance
    }
}    

//in project TestMyProj           
public void MyFuncTest()
{
    TypeA_Accessor target = new TypeA_Accessor();
    //following line is the one that throws exception
    target.myList.Add(new TypeA_Accessor.TypeB());
    target.MyFunc();

    //check changed state of target
}

运行时错误是:

Object of type System.Collections.Generic.List`1[MyProj.TypeA.TypeA_Accessor+TypeB]' cannot be converted to type 'System.Collections.Generic.List`1[MyProj.TypeA.TypeA+TypeB]'.

根据智能感知 - 因此我猜是编译器 - 目标是 TypeA_Accessor 类型。但在运行时它是 TypeA 类型,因此列表添加失败。

有什么办法可以阻止这个错误?或者,也许更有可能,其他人有什么其他建议(我预测可能“不要测试私有方法”和“不要让单元测试操纵对象的状态”)。

您需要私有类 TypeB 的访问器。访问器 TypeA_Accessor 提供对 TypeA 的私有和受保护方法的访问。但是 TypeB 不是一种方法。这是一个类。
访问器提供对私有/受保护方法、成员、属性和事件的访问。它不提供对您班级中的私有/受保护班级的访问。私有/受保护类 (TypeB) 仅供拥有类 (TypeA) 的方法使用。所以基本上你正在尝试将私有类(TypeB)从 TypeA 之外添加到私有的“myList”中。由于您使用的是访问器,因此访问 myList 没有问题。但是,您不能通过访问器使用 TypeB。可能的解决方案是将 TypeB 移到 TypeA 之外。但它会破坏你的设计。
感觉测试私有方法应该通过下面的stackoverflow.com/questions/250692/…来完成

p
phoenix

您可以使用 PrivateObject 类:

Class target = new Class();
PrivateObject obj = new PrivateObject(target);
var retVal = obj.Invoke("PrivateMethod");
Assert.AreEqual(expectedVal, retVal);

注意:PrivateObjectPrivateType 不适用于面向 netcoreapp2.0 - GitHub Issue 366 的项目


这是正确的答案,因为微软已经添加了 PrivateObject。
很好的答案,但请注意 PrivateMethod 需要“保护”而不是“私有”。
@HerbalMart:也许我误解了您,但是如果您建议 PrivateObject 只能访问受保护的成员而不能访问私有成员,那您就错了。
@JeffPearce 对于静态方法,您可以使用“PrivateType pt = new PrivateType(typeof(MyClass));”,然后在 pt 对象上调用 InvokeStatic,就像在私有对象上调用 Invoke 一样。
如果有人想知道从 MSTest.TestFramework v1.2.1 开始 - PrivateObject 和 PrivateType 类对于面向 .NET Core 2.0 的项目不可用 - 有一个 github 问题:github.com/Microsoft/testfx/issues/366
C
Community

“没有所谓的标准或最佳实践,可能它们只是流行的观点”。

本次讨论也是如此。

https://i.stack.imgur.com/MA56m.png

这完全取决于您认为什么是 unit ,如果您认为 UNIT 是一个类,那么您只会点击 public 方法。如果你认为 UNIT 是一行代码,碰到私有方法不会让你感到内疚。

如果要调用私有方法,可以使用“PrivateObject”类并调用调用方法。您可以观看这个深入的 youtube 视频 (http://www.youtube.com/watch?v=Vq6Gcs9LrPQ),它展示了如何使用“PrivateObject”,还讨论了私有方法的测试是否合乎逻辑。


在我们这个领域里,所有迂腐的玩意导致的问题多于解决的问题。这就是为什么我的一位编程老师说:“让一切都公开!”
C
Code-Apprentice

这里的另一个想法是将测试扩展到“内部”类/方法,给这种测试更多的白盒感觉。您可以使用程序集上的 InternalsVisibleTo 属性将它们公开给单独的单元测试模块。

结合密封类,您可以进行这样的封装,即测试方法仅在您的方法的单元测试程序集中可见。考虑密封类中的受保护方法实际上是私有的。

[assembly: InternalsVisibleTo("MyCode.UnitTests")]
namespace MyCode.MyWatch
{
    #pragma warning disable CS0628 //invalid because of InternalsVisibleTo
    public sealed class MyWatch
    {
        Func<DateTime> _getNow = delegate () { return DateTime.Now; };
    

       //construktor for testing purposes where you "can change DateTime.Now"
       internal protected MyWatch(Func<DateTime> getNow)
       {
           _getNow = getNow;
       }

       public MyWatch()
       {            
       }
   }
}

和单元测试:

namespace MyCode.UnitTests
{

[TestMethod]
public void TestminuteChanged()
{
    //watch for traviling in time
    DateTime baseTime = DateTime.Now;
    DateTime nowforTesting = baseTime;
    Func<DateTime> _getNowForTesting = delegate () { return nowforTesting; };

    MyWatch myWatch= new MyWatch(_getNowForTesting );
    nowforTesting = baseTime.AddMinute(1); //skip minute
    //TODO check myWatch
}

[TestMethod]
public void TestStabilityOnFebruary29()
{
    Func<DateTime> _getNowForTesting = delegate () { return new DateTime(2024, 2, 29); };
    MyWatch myWatch= new MyWatch(_getNowForTesting );
    //component does not crash in overlap year
}
}

是的,这就是我的建议。这有点“hacky”,但至少它们不是“public”。
这是一个很好的答案,因为它没有说“不要测试私有方法”,但是是的,它很“hacky”。我希望有一个解决方案。 IMO 说“不应该测试私有方法”是不好的,因为在我看来:它等同于“私有方法不应该是正确的”。
ya ken,我也对那些声称不应该在单元测试中测试私有方法的人感到困惑。公共 API 是输出,但有时错误的实现也会给出正确的输出。或者实现产生了一些不好的副作用,例如持有不必要的资源,引用对象阻止它被 gc 收集......等等。除非他们提供其他可以涵盖私有方法而不是单元测试的测试,否则我会认为他们无法维护 100% 测试过的代码。
好的。我没有意识到这一点。正是我需要的。假设我构建了一个 3rd 方组件供其他人使用,但我只想公开一个非常有限的 API。在内部它非常复杂,我想测试单个组件(例如输入解析器或验证器),但我不想公开这些。最终用户不需要了解这些。我知道这里的标准方法是“只测试你的公共 API”,但我更喜欢测试单一职责的代码单元。这使我可以在不公开它们的情况下做到这一点。
我认为这也应该是公认的答案。关于私有方法测试的辩论读起来很酷,但更多的是见仁见智。正如我所看到的,双方都提出了很好的论据。如果你仍然想/需要测试私有方法,你应该有办法。这是唯一提供的。此外,您可以在 AssemblyInfo.cs 中使用:[assembly: InternalsVisibleTo("UT.cs")] 对整个程序集执行此操作
J
Jack Davidson

测试私有方法的一种方法是通过反射。这也适用于 NUnit 和 XUnit:

MyObject objUnderTest = new MyObject();
MethodInfo methodInfo = typeof(MyObject).GetMethod("SomePrivateMethod", BindingFlags.NonPublic | BindingFlags.Instance);
object[] parameters = {"parameters here"};
methodInfo.Invoke(objUnderTest, parameters);

call methods 静态非静态
依赖反射的方法的缺点是,当您使用 R# 重命名方法时,它们往往会中断。在小型项目中这可能不是什么大问题,但在大型代码库中,让单元测试以这种方式中断然后不得不四处走动并快速修复它们变得有点烦人。从这个意义上说,我的钱花在了杰夫的回答上。
@XDS 太糟糕了 nameof() 无法从其类外部获取私有方法的名称。
C
CP70

Ermh... 遇到了完全相同的问题:测试一个简单,但关键的private 方法。看完这个帖子,好像是“我想在这块简单的金属上钻一个简单的孔,我想确保质量符合规格”,然后“好吧,这不是很简单。首先,没有合适的工具可以做到这一点,但是你可以在你的花园里建造一个引力波天文台。阅读我在 http://foobar.brigther-than-einstein.org/ 的文章,当然首先,你必须参加一些高级量子物理课程,那么你需要大量的超冷氮,当然,我的书可以在亚马逊上买到”......

换句话说...

不,第一件事。

每一种方法,可能是私有的、内部的、受保护的、公共的,都必须是可测试的。必须有一种方法来实现这样的测试,而不用像这里介绍的那样麻烦。

为什么?正是因为一些贡献者到目前为止所做的架构提及。也许对软件原则的简单重申可能会消除一些误解。

在这种情况下,通常的嫌疑人是:OCP、SRP,以及一如既往的 KIS。

但是等一下。将所有东西都公开的想法更多的是不那么政治化,而是一种态度。但。谈到代码,即使在当时的开源社区,这也不是教条。相反,“隐藏”某些东西是一种很好的做法,可以更容易地熟悉某个 API。例如,您会隐藏新上市的数字温度计构建模块的核心计算——不是为了向好奇的代码读者隐藏真实测量曲线背后的数学,而是为了防止您的代码变得依赖于某些,或许突然间重要的用户无法抗拒使用您以前私有的、内部的、受保护的代码来实现他们自己的想法。

我在说什么?

私人双 TranslateMeasurementIntoLinear(双实际测量);

很容易宣布水瓶座时代或现在所谓的时代,但如果我的传感器从 1.0 升级到 2.0,Translate... 的实现可能会从一个易于理解的简单线性方程变为“重新-对每个人都可用”,到使用分析或其他东西的非常复杂的计算,所以我会破坏其他人的代码。为什么?因为他们不了解软件编码的基本原理,甚至不了解 KIS。

为了让这个童话故事变得简短:我们需要一种简单的方法来测试私有方法——不用多说。

第一:大家新年快乐!

第二:排练你的建筑师课程。

第三:“公共”修饰语是宗教,而不是解决方案。


我完全同意。我也想知道我们应该怎么做,以防销售包含代码的产品。我们必须清楚公共 API,但对内部功能进行大量测试也很棒。如果一切都经过测试,而且很小,它可能会正常工作。但是那些没有人需要调用的小功能也需要测试!
选择你的毒药。具有严格访问权限的代码,但单元测试很薄,或者对具有完全公共方法的类进行大量单元测试。不完全是专业人士想要做出的选择。
R
Roger Hill

另一个没有提到的选项是创建单元测试类作为您正在测试的对象的子类。 NUnit 示例:

[TestFixture]
public class UnitTests : ObjectWithPrivateMethods
{
    [Test]
    public void TestSomeProtectedMethod()
    {
        Assert.IsTrue(this.SomeProtectedMethod() == true, "Failed test, result false");
    }
}

这将允许轻松测试私有和受保护(但不是继承的私有)方法,并且它允许您将所有测试与真实代码分开,因此您不会将测试程序集部署到生产中。在许多继承对象中,将私有方法切换为受保护方法是可以接受的,而且这是一个非常简单的更改。

然而...

虽然这是解决如何测试隐藏方法的问题的有趣方法,但我不确定我是否会主张这是在所有情况下解决问题的正确方法。在内部测试一个对象似乎有点奇怪,我怀疑这种方法可能会在某些情况下对你产生影响。 (例如,不可变对象可能会使某些测试变得非常困难)。

虽然我提到了这种方法,但我认为这更像是一个头脑风暴的建议,而不是一个合法的解决方案。带上一粒盐。

编辑:我发现人们投票否决这个答案真的很搞笑,因为我明确地将其描述为一个坏主意。这是否意味着人们同意我的观点?我感到很困惑.....


这是一个创造性的解决方案,但有点 hacky。
这真的很好用。我已经根据需要做了几次。好处是重命名目标类或被测方法不会破坏任何东西,正如人们在反射中看到的那样。
K
Kai Hartmann

来自《有效地使用遗留代码》一书:

“如果我们需要测试一个私有方法,我们应该将它公开。如果公开它让我们感到困扰,在大多数情况下,这意味着我们的班级做得太多,我们应该修复它。”

根据作者的说法,修复它的方法是创建一个新类并将方法添加为 public

作者进一步解释:

“好的设计是可测试的,不可测试的设计是坏的。”

因此,在这些限制内,您唯一真正的选择是在当前类或新类中创建方法 public


安全编码实践要求我们应该将所有内容设为私有,除非它绝对需要公开,例如 API 接口。如果你程序中的每一件小事都被定义为公开的,那么恶意行为者可能会调用它来使用你的代码来攻击系统。您希望在可测试的同时减少二进制文件的攻击面。
@hackslash 将事物设为私有不会隐藏任何内容,也不会禁止其他程序集或对象实际调用它们。它只是添加了编译时类型检查。
这是面对这个问题时要考虑的合理建议,但绝对不是首选答案。本质上它说“永远不要使用私有方法”(或者:不要测试所有方法)。在童话学术界或内部应用程序开发场景中可能会起作用。但是对于具有公共 API 的复杂库,请忘记它。想象一下,您已经有一个复杂的库,其中包含相互依赖的类的大型对象图。现在通过将所有私有代码移动到新的虚拟类并以某种方式与它们共享状态来使其变得更加困难。
这个响应没有回答最初的问题(“我如何测试私有方法?”),而是回答了没有人问过的问题(“我应该测试私有方法吗?”)。
l
lstanczyk

我使用这个助手(对象类型扩展)

 public static  TReturn CallPrivateMethod<TReturn>(
        this object instance,
        string methodName,
        params object[] parameters)
    {
        Type type = instance.GetType();
        BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Instance;
        MethodInfo method = type.GetMethod(methodName, bindingAttr);

        return (TReturn)method.Invoke(instance, parameters);
    }

你可以这样称呼它

Calculator systemUnderTest = new Calculator();
int result = systemUnderTest.CallPrivateMethod<int>("PrivateAdd",1,8);

优点之一是它使用泛型来预先确定返回类型。


D
Darkgaze

将私有方法提取到另一个类,在该类上进行测试;阅读更多关于 SRP 原则(单一职责原则)的信息

看来您需要将 private 方法提取到另一个类;这应该是 public。您应该测试另一个类的 public 方法,而不是尝试测试 private 方法。

我们有以下场景:

Class A
+ outputFile: Stream
- _someLogic(arg1, arg2) 

我们需要测试_someLogic的逻辑;但似乎 Class A 的作用超出了它的需要(违反了 SRP 原则);只需重构为两个类

Class A1
    + A1(logicHandler: A2) # take A2 for handle logic
    + outputFile: Stream
Class A2
    + someLogic(arg1, arg2) 

这样 someLogic 可以在 A2 上进行测试;在 A1 中创建一些假 A2,然后注入构造函数以测试 A2 是否被调用到名为 someLogic 的函数。


您将如何测试一个场景,其中类的职责是生成数据(它是私下完成的,例如,通过处理来自某个第三方库的事件)然后以已知格式公开该数据,并且您想要测试那个公共接口?这里没有一个答案声称我们不应该测试方法来解释生产者场景。
@allmhuran:您应该再提供一个包含输入事件生产者的构造函数,例如: p = ProduceData(inputEventProducer);然后通过调用 inputEventProducer.emit("some event" ) 模拟事件发生并测试 p 的输出。一般来说,如果很难测试,则代码存在一些问题,我们需要重构。或者,如果您无法修改代码,则需要进行一些更高级别的测试,例如集成或 E2E。
但该构造函数不会成为生产应用程序正常流程的一部分。您基本上只是为了测试而将模拟编码到非模拟类中。这就是为什么能够在测试中调用私有方法是有意义的……因为这就是生产中实际发生的事情。也就是说,是的,翻译器取了一个注入的依赖,但是注入的依赖来自第三方库(可能是类,不是接口)。
P
Patrick Knott
public static class PrivateMethodTester
{
    public static object InvokePrivateMethodWithReturnType<T>(this T testObject, string methodName, Type[] methodParamTypes, object[] parameters)
    {
        //shows that we want the nonpublic, static, or instance methods.
        var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance;

        //gets the method, but we need the methodparamtypes so that we don't accidentally get an ambiguous method with different params.
        MethodInfo methodInfo = testObject.GetType().GetMethod(methodName, flags, null, methodParamTypes, null);
        if (methodInfo == null)
        {
            throw new Exception("Unable to find method.");
        }

        //invokes our method on our object with the parameters.
        var result = methodInfo.Invoke(testObject, parameters);
        if (result is Task task)
        {
            //if it is a task, it won't resolve without forcing it to resolve, which means we won't get our exceptions.
            task.GetAwaiter().GetResult();
        }

        return result;
    }
}

这样称呼它:

        Type[] paramTypes = new Type[] { typeof(OrderTender), typeof(string) };
        var parameters = new object[] { orderTender, OrderErrorReasonNames.FailedToCloneTransaction };

        myClass.InvokePrivateMethodWithReturnType("myPrivateMethodName", paramTypes, parameters);

A
Allen

在 VS 2005/2008 中,您可以使用 private accessor 来测试私有成员,但这种方式在以后的 VS 版本中消失


早在 2008 年到 2010 年初就有很好的答案。现在请参阅 PrivateObject 和 Reflection 替代方案(参见上面的几个答案)。 VS2010 有访问器错误,MS 在 VS2012 中弃用了它。除非您被迫留在 VS2010 或更早版本(>18 岁的构建工具),否则请避免使用私人访问者来节省时间。 :-)。
c
cyril.andreichuk

您可以使用嵌套类来测试私有方法。例如(使用 NUnit v3):

    
    internal static class A
    {
        // ... other code

        private static Int32 Sum(Int32 a, Int32 b) => a + b;

        [TestFixture]
        private static class UnitTests
        {
            [Test]
            public static void OnePlusTwoEqualsThree()
            {
                Assert.AreEqual(3, Sum(1, 2));
            }
        }
    }

此外,可以使用“部分类”功能将测试相关代码移动到另一个文件,使用“条件编译”等从发布版本中排除。高级示例:

文件 A.cs

    
    internal static partial class A
    {
        // ... other code

        private static Int32 Sum(Int32 a, Int32 b) => a + b;
    }

文件 A.UnitTests.cs


#if UNIT_TESTING
    partial class A
    {
        [TestFixture]
        private static class UnitTests
        {
            [Test]
            public static void OnePlusTwoEqualsThree()
            {
                Assert.AreEqual(3, Sum(1, 2));
            }
        }
    }
#endif


A
Amirhossein Yari

我有另一种对我有用的方法。因为我总是在调试模式下运行我的测试,所以我使用 #if DEBUG 在我的私有方法之前添加 public。所以我的私有方法是这样的:

public class Test
{
    #if (DEBUG)
      public
    #endif
    string PrivateMehtod()
    {
      return "PrivateMehtod called";
    }
}

有时您希望在发布模式下运行测试,以便也测试优化的代码。
是的,此代码适用于调试模式,但您可以添加另一种在发布和优化模式下运行的模式,并将其仅用于测试。例如将其更改为 #if (DEBUG || OptimizeMode) 并在调试模式下进行测试,最后确保在 OptimizeMode 中进行测试
这可能对两个新的解决方案配置有意义,DEBUG_TEST 和 DEBUG_RELEASE。否则,它将正常运行。
P
PhOeNiX

遗憾的是 .net6 中没有 PrivateObject

但是,我编写了一个能够使用反射调用私有方法的小型扩展方法。

看一下示例代码:

class Test
{
  private string GetStr(string x, int y) => $"Success! {x} {y}";
}

var test = new Test();
var res = test.Invoke<string>("GetStr", "testparam", 123);
Console.WriteLine(res); // "Success! testparam 123"

这是扩展方法的实现:

/// <summary>
/// Invokes a private/public method on an object. Useful for unit testing.
/// </summary>
/// <typeparam name="T">Specifies the method invocation result type.</typeparam>
/// <param name="obj">The object containing the method.</param>
/// <param name="methodName">Name of the method.</param>
/// <param name="parameters">Parameters to pass to the method.</param>
/// <returns>The result of the method invocation.</returns>
/// <exception cref="ArgumentException">When no such method exists on the object.</exception>
/// <exception cref="ArgumentException">When the method invocation resulted in an object of different type, as the type param T.</exception>
/// <example>
/// class Test
/// {
///   private string GetStr(string x, int y) => $"Success! {x} {y}";
/// }
///
/// var test = new Test();
/// var res = test.Invoke&lt;string&gt;("GetStr", "testparam", 123);
/// Console.WriteLine(res); // "Success! testparam 123"
/// </example>
public static T Invoke<T>(this object obj, string methodName, params object[] parameters)
{
  var method = obj.GetType().GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
  if (method == null)
  {
    throw new ArgumentException($"No private method \"{methodName}\" found in class \"{obj.GetType().Name}\"");
  }

  var res = method.Invoke(obj, parameters);
  if (res is T)
  {
    return (T)res;
  }

  throw new ArgumentException($"Bad type parameter. Type parameter is of type \"{typeof(T).Name}\", whereas method invocation result is of type \"{res.GetType().Name}\"");
}

S
Soma Mbadiwe

现在是2022年!

...我们有 .NET6

虽然这并不能真正回答问题,但这些天我首选的方法是在同一个 C# 项目中配置代码和测试,命名约定类似于 <ClassName>.Tests.cs。然后我使用 internal 访问修饰符而不是 private

在项目文件中,我有这样的东西:

<ItemGroup Condition="'$(Configuration)' == 'Release'">
  <Compile Remove="**\*.Tests.cs" />
</ItemGroup>

排除 release 构建中的测试文件。根据需要进行修改。

常见问题 1:但有时您还想在发布(优化)构建中测试代码。

答:我觉得没必要。我相信编译器会在不打乱我意图的情况下完成它的工作。到目前为止,我没有理由质疑它这样做的能力。

FAQ 2:但我真的想保留方法(或类)private

答案:此页面中有很多出色的解决方案可供尝试。根据我的经验,将访问修饰符设置为 internal 通常就足够了,因为方法(或类)在它定义的项目之外是不可见的。除此之外,没有什么可隐藏的了。