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

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


当前回答

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

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

根据作者的说法,修复它的方法是创建一个新类并将该方法添加为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);

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

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

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

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

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

作者进一步解释说:

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

因此,在这些限制范围内,您唯一真正的选择是将方法设为公共的,无论是在当前类中还是在新类中。

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

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

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

然而……

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

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

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

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