我正在构建一个类库,它将有一些公共和私有方法。我希望能够对私有方法进行单元测试(主要是在开发过程中,但也可以用于将来的重构)。

正确的做法是什么?


当前回答

在c#中,您可以使用我下面提供的代码。尽管我认为只有在绝对需要的时候才应该进行私有方法的单元测试。我遇到过一些我觉得有必要这样做的案例。下面是我在UnitTestBase类中创建的一些c#方法,我继承了我的UnitTest类(你也可以把它放在一个静态的“助手”类中)。HTH

protected internal static TResult? InvokePrivateInstanceMethod<TType, TResult>(string methodName, object?[]? methodArguments = null, params object?[]? constructorArguments)
{
    var classType = typeof(TType);
    var instance = Activator.CreateInstance(classType, constructorArguments);
    var privateMethodInfo = classType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
                                        .FirstOrDefault(m => m.IsPrivate &&
                                            m.Name.Equals(methodName, StringComparison.Ordinal) &&
                                            m.ReturnType.Equals(typeof(TResult)));
 
    if (privateMethodInfo is null)
    {
        throw new MissingMethodException(classType.FullName, methodName);
    }

    var methodResult = privateMethodInfo.Invoke(instance, methodArguments);
    if (methodResult is not null)
    {
        return (TResult)methodResult;
    }

    return default;
}

protected internal static async Task<TResult?> InvokePrivateInstanceMethodAsync<TType, TResult>(string methodName, object?[]? methodArguments = null, params object?[]? constructorArguments)
{
    var classType = typeof(TType);
    var instance = Activator.CreateInstance(classType, constructorArguments);
    var privateMethodInfo = classType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
                                        .FirstOrDefault(m => m.IsPrivate &&
                                            m.Name.Equals(methodName, StringComparison.Ordinal) &&
                                            m.ReturnType.Equals(typeof(Task<TResult>)));
            
    if (privateMethodInfo is null)
    {
        throw new MissingMethodException(classType.FullName, methodName);
    }

    var methodResult = privateMethodInfo.Invoke(instance, methodArguments);
    if (methodResult is not null)
    {
        return await (Task<TResult>)methodResult;
    }

    return default;
}

其他回答

我还使用了InternalsVisibleToAttribute方法。值得一提的是,如果你为了达到这个目的而将你之前的私有方法变成内部方法,那么也许它们不应该成为直接单元测试的对象。

毕竟,您是在测试类的行为,而不是它的具体实现——您可以在不更改前者的情况下更改后者,并且您的测试仍然应该通过。

将它们声明为内部的,然后使用InternalsVisibleToAttribute允许单元测试程序集看到它们。

对于任何想要运行私有方法的人来说。这适用于任何单元测试框架,只使用旧的Reflection。

public class ReflectionTools
{
    // If the class is non-static
    public static Object InvokePrivate(Object objectUnderTest, string method, params object[] args)
    {
        Type t = objectUnderTest.GetType();
        return t.InvokeMember(method,
            BindingFlags.InvokeMethod |
            BindingFlags.NonPublic |
            BindingFlags.Instance |
            BindingFlags.Static,
            null,
            objectUnderTest,
            args);
    }
    // if the class is static
    public static Object InvokePrivate(Type typeOfObjectUnderTest, string method, params object[] args)
    {
        MemberInfo[] members = typeOfObjectUnderTest.GetMembers(BindingFlags.NonPublic | BindingFlags.Static);
        foreach(var member in members)
        {
            if (member.Name == method)
            {
                return typeOfObjectUnderTest.InvokeMember(method, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.InvokeMethod, null, typeOfObjectUnderTest, args);
            }
        }
        return null;
    }
}

然后在你的实际测试中,你可以这样做:

Assert.AreEqual( 
  ReflectionTools.InvokePrivate(
    typeof(StaticClassOfMethod), 
    "PrivateMethod"), 
  "Expected Result");

Assert.AreEqual( 
  ReflectionTools.InvokePrivate(
    new ClassOfMethod(), 
    "PrivateMethod"), 
  "Expected Result");

我很惊讶还没有人说过这一点,但我采用的一个解决方案是在类内部创建一个静态方法来测试自身。这使您可以访问用于测试的所有公共和私有内容。

此外,在脚本语言(具有OO功能,如Python、Ruby和PHP)中,您可以在运行时对文件本身进行测试。确保您的更改不会破坏任何东西的快速方法。这显然为测试所有类提供了可伸缩的解决方案:只需运行所有类即可。(你也可以用void main在其他语言中这样做,它也总是运行它的测试)。

我使用PrivateObject类。但如前所述,最好避免测试私有方法。

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