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

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


当前回答

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

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

其他回答

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

看起来你需要将私有方法提取到另一个类;在这方面应该是公开的。您应该测试另一个类的公共方法,而不是试图测试私有方法。

我们有以下场景:

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

我们需要测试_someLogic;但A类似乎扮演了过多的角色(违反SRP原则);只需将其重构为两个类

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

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

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

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

遗憾的是。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。

作者进一步解释说:

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

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