【问题标题】:How do I unit test a C# function which returns a Func<something>?如何对返回 Func<something> 的 C# 函数进行单元测试?
【发布时间】:2010-03-22 12:01:53
【问题描述】:

我有一个类,其中包含一个方法,该方法返回一个包含 Func 类型属性的 Result 对象。

class Result {
   public Func<Result> NextAction { get; set; }
}

如何编写有关此 Func 内容的单元测试断言?以下显然不起作用,因为编译器为 lambda 生成了两种不同的方法:

// Arrange
ListController controller = new ListController(domain);
// Act
Result actual = controller.DefaultAction();
// Assert
Func<Result> expected = () => new ProductsController(domain).ListAction();
Assert.That(actual.NextAction, Is.EqualTo(expected));

我猜我可以通过使用表达式树来做到这一点,但是......有没有办法避免这样做?我正在使用 NUnit 2.5。

编辑: Result 对象中没有其他标识字段。它旨在成为一种基于当前对象/方法中做出的决定调用下一个对象/方法的方式。

【问题讨论】:

    标签: c# unit-testing lambda


    【解决方案1】:

    为什么不调用Func 并比较返回值?

    var actualValue = actual.NextAction();
    var expectedValue = expected();
    Assert.That(actualValue, Is.EqualTo(expectedValue));
    

    编辑:我看到 Result 类没有任何标识。我猜您在 Result 类中还有一些其他字段,它们定义了 Result 的标识,可用于确定两个结果是否相等。

    【讨论】:

    • 因为Func返回另一个Result对象,同样的问题!
    • 然后你需要实现一些东西来识别 Result 并根据它来确定相等性
    • 像描述 Func 意图的任意字符串一样?
    • Result 类中没有标识字段。它旨在作为一个占位符,用于根据当前类/方法中的决定动态调用下一个类/方法
    • 如果域中没有定义对象身份的任何内容,则应该使用任意字符串。但是我怀疑您正在尝试对不打算进行单元测试的东西进行单元测试 - 最后,程序必须有一个可用的值,并且您应该断言这些值,而不是提供它们的 Funcs。
    【解决方案2】:

    我不知道有一种简单的方法来查看 lambda 内部(除了使用您所说的表达式树),但如果他们被分配了 method group ,则可以比较委托。

    var result1 = new Result {
        NextAction = new ProductsController(domain).ListAction };
    var result2 = new Result {
        NextAction = new ProductsController(domain).ListAction };
    
    //objects are different
    Assert.That(result1, Is.Not.EqualTo(result2));
    
    //delegates are different
    Assert.That(result1.NextAction, Is.Not.EqualTo(result2.NextAction));
    
    //methods are the same
    Assert.That(result1.NextAction.Method, Is.EqualTo(result2.NextAction.Method));
    

    如果您使用 lambda,上述示例将不起作用,因为它们被编译为不同的方法。

    【讨论】:

    • 是的,这很像测试事件注册或任何其他委托使用。虽然这可能是最好的折衷方案,但最初的意图是将 ProductsController 的实例化作为调用 NextAction 的一部分。
    【解决方案3】:

    如果您Func&lt;Result&gt; 总是返回相同的结果,您可以测试该函数返回的是哪个对象。

    【讨论】:

    • 不幸的是,Func 返回的 Result 只包含另一个 Func
    【解决方案4】:

    看来对Func 的内容进行单元测试超出了单元测试的正常范围。 Func 表示已编译的代码,因此如果不求助于解析 MSIL,就无法进一步检查。在这种情况下,因此有必要使用委托和实例化类型(如 Nathan Baulch 所建议的那样),或者改用表达式树。

    我的表达式树等效如下:

    class Result {
       public Expression<Func<Result>> NextAction { get; set; }
    }
    

    单元测试如下:

    // Arrange
    ListController controller = new ListController(domain);
    // Act
    Result actual = controller.DefaultAction();
    // Assert
    MethodCallExpression methodExpr = (MethodCallExpression)actual.NextAction.Body;
    NewExpression newExpr = (NewExpression)methodExpr.Object;
    Assert.That(newExpr.Type, Is.EqualTo(typeof(ProductsController)));
    Assert.That(methodExpr.Method.Name, Is.EqualTo("ListAction"));
    

    请注意,此测试存在一些固有的脆弱性,因为它暗示了表达式的结构及其行为。

    【讨论】:

      【解决方案5】:

      如果我正确理解了这个问题,NextAction 可能有也可能没有不同的 lambda 实现,这是需要测试的。

      在下面的示例中,我比较了方法 IL 字节。使用反射,从数组中的主体获取方法信息和 IL 字节。如果字节数组匹配,则 lambda 是相同的。

      这无法处理很多情况,但如果只是比较两个应该完全相同的 lambda 的问题,这将起作用。抱歉,它在 MSTest 中 :)

      using System.Reflection;
      ....
      
      
          [TestClass]
          public class Testing
          {
              [TestMethod]
              public void Results_lambdas_match( )
              {
                  // Arrange 
                  ListController testClass = new ListController( );
                  Func<Result> expected = ( ) => new ProductsController( ).ListAction( );
                  Result actual;
                  byte[ ] actualMethodBytes;
                  byte[ ] expectedMethodBytes;
      
                  // Act 
                  actual = testClass.DefaultAction( );
      
                  // Assert
                  actualMethodBytes = actual.NextAction.
                      Method.GetMethodBody( ).GetILAsByteArray( );
                  expectedMethodBytes = expected.
                      Method.GetMethodBody( ).GetILAsByteArray( );
      
                  // Test that the arrays are the same, more rigorous check really should
                  // be done .. but this is an example :)
                  for ( int count=0; count < actualMethodBytes.Length; count++ )
                  {
                      if ( actualMethodBytes[ count ] != expectedMethodBytes[ count ] )
                          throw new AssertFailedException(
                             "Method implementations are not the same" );
                  }
              }
              [TestMethod]
              public void Results_lambdas_do_not_match( )
              {
                  // Arrange 
                  ListController testClass = new ListController( );
                  Func<Result> expected = ( ) => new OtherController( ).ListAction( );
                  Result actual;
                  byte[ ] actualMethodBytes;
                  byte[ ] expectedMethodBytes;
                  int count=0;
      
                  // Act 
                  actual = testClass.DefaultAction( );
      
                  // Assert
                  actualMethodBytes = actual.NextAction.
                      Method.GetMethodBody( ).GetILAsByteArray( );
                  expectedMethodBytes = expected.
                      Method.GetMethodBody( ).GetILAsByteArray( );
      
                  // Test that the arrays aren't the same, more checking really should
                  // be done .. but this is an example :)
                  for ( ; count < actualMethodBytes.Length; count++ )
                  {
                      if ( actualMethodBytes[ count ] != expectedMethodBytes[ count ] )
                          break;
                  }
                  if ( ( count + 1 == actualMethodBytes.Length ) 
                      && ( actualMethodBytes.Length == expectedMethodBytes.Length ) )
                      throw new AssertFailedException(
                          "Method implementations are the same, they should NOT be." );
              }
      
              public class Result
              {
                  public Func<Result> NextAction { get; set; }
              }
              public class ListController
              {
                  public Result DefaultAction( )
                  {
                      Result result = new Result( );
                      result.NextAction = ( ) => new ProductsController( ).ListAction( );
      
                      return result;
                  }
              }
              public class ProductsController
              {
                  public Result ListAction( ) { return null; }
              }
              public class OtherController
              {
                  public Result ListAction( ) { return null; }
              }
          }
      

      【讨论】:

      • 是的,这有可能。这实际上可以工作,除了代码将“域”参数传递给 ProductsController 的构造函数,该构造函数解析为不同的 IL。我相信这是由于创建了用于托管已编译 lambda 表达式的类。
      • 解决此问题的一种方法是从正在运行的代码中“捕获”正确的 IL 字节代码并与之匹配。测试最终会非常具体地针对正在测试的条件,但会起作用。如果你有一个针对特定状态的测试,这还不错,但我不想要很多。
      猜你喜欢
      • 2020-09-28
      • 1970-01-01
      • 2021-12-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-25
      • 1970-01-01
      相关资源
      最近更新 更多