在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,因此列表添加失败。

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

328746 次浏览

这里的另一个想法是将测试扩展到“内部”。类/方法,提供了更多的白盒测试的感觉。你可以在程序集上使用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
}
}

你可以使用PrivateObject类:

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

注意:PrivateObjectPrivateType不适用于针对netcoreapp2.0 - GitHub第366期的项目

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

同样的道理也适用于这个讨论。

enter image description here

这取决于你认为什么是单元,如果你认为unit是一个类,那么你只会碰到公共方法。如果你认为UNIT是代码行,敲打私有方法不会让你感到内疚。

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

在VS 2005/2008中,你可以使用私有访问器来测试私有成员,但这种方法在VS的后期版本中是消失

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

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

提取私有方法到另一个类,在该类上进行测试;阅读更多关于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)

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

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

换句话说……

不,重要的事先做。

每一个方法,可以是私有的,内部的,受保护的,公共的可测试。必须有一种方法可以毫不费力地实现这样的测试,就像这里介绍的那样。

为什么?到目前为止,一些贡献者所做的架构提到的因为。也许简单地重申一下软件原则就可以消除一些误解。

在这种情况下,通常的怀疑对象是:OCP、SRP和KIS。

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

我在说什么?

private double translatemmeasurementintolinear (double actualMeasurement);

现在很容易宣布水瓶座时代或被称为什么,但如果我的传感器从1.0到2.0,翻译的实现……可能会从一个简单的易于理解和对每个人都“可重复使用”的线性方程,变成一个相当复杂的计算,使用分析或其他东西,所以我会破坏别人的代码。为什么?因为他们不懂软件编码的基本原理,甚至KIS也不懂。

为了让这个童话故事简短:我们需要一个简单的方法来测试私有方法——毫不费力。

第一:大家新年快乐!

第二:预习你的建筑师课程。

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

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

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

这将允许轻松测试私有和受保护的(但不继承私有)方法,并且允许将所有测试与实际代码分开,这样就不必将测试程序集部署到生产环境中。在许多继承对象中,将私有方法切换为受保护的方法是可以接受的,而且这是一个相当简单的更改。

然而……

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

虽然我提到了这种方法,但我认为这更像是一个头脑风暴的建议,而不是一个合理的解决方案。对它持保留态度。

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

选自书有效地使用遗留代码:

"如果我们需要测试一个私有方法,我们应该把它设为公共。如果 让它公开让我们很困扰,在大多数情况下,这意味着我们的类是 做得太多了,我们应该解决它。" < / p >

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

作者进一步解释说:

“好的设计是可测试的,不能测试的设计是糟糕的。”

因此,在这些限制范围内,你唯一真正的选择是在当前类或新类中使方法public

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

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

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

你可以使用嵌套类来测试私有方法。例如(使用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));
}
}
}

此外,可以使用“部分类”特性将测试相关代码移动到另一个文件中,使用“条件编译”将其排除在发布版本之外,等等。先进的例子:

文件交流


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


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

public class Test
{
#if (DEBUG)
public
#endif
string PrivateMehtod()
{
return "PrivateMehtod called";
}
}
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);

遗憾的是。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}\"");
}

现在是2022年了!

...我们有。net 6

虽然这并没有真正回答问题,但我现在更喜欢的方法是在同一个c#项目中搭配代码和测试,使用<ClassName>.Tests.cs这样的命名约定。然后我使用internal访问修饰符而不是private

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

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

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

常见问题1:但有时你也想在发布(优化)版本中测试代码。

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

常见问题2:但是我真的想保留方法(或类)private

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

如果PrivateObject不可用,并且被测试的类不是密封类,则可以将想要公开的方法和属性设置为受保护的。在单元测试文件中创建一个带有内部方法的继承类,这些内部方法公开了测试中的私有方法/属性。

如果被测试的类是:

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

改为:

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

在单元测试文件中创建一个继承类,如下所示:

class MyClassExposed: MyClass
{
internal string ExposedGetStr(string x, int y)
{
return base.GetStr(x, y);
}
}

现在您可以使用继承的类MyClassExposed来测试公开的方法和属性。