在运行时动态调用Moq的Setup()方法

6

我希望创建一个工厂,用于为我的单元测试创建常见的模拟对象。我已经成功设置了我的测试,可以模拟 Linq2Sql DataContext 并返回内存表而不是访问数据库。我是这样设置的:

_contactsTable = new InMemoryTable<Contact>(new List<Contact>());
_contactEmailsTable = new InMemoryTable<ContactEmail>(new List<ContactEmail>());
//  repeat this for each table in the ContactsDataContext

var mockContext = new Mock<ContactsDataContext>();
mockContext.Setup(c => c.Contacts).Returns(_contactsTable);
mockContext.Setup(c => c.ContactEmails).Returns(_contactEmailsTable);
// repeat this for each table in the ContactsDataContext

如果DataContext包含很多表,使用它会变得繁琐。因此,我认为可以使用一个简单的工厂方法,利用反射从DataContext中获取所有表:

public static DataContext GetMockContext(Type contextType)
{
    var instance = new Mock<DataContext>();
    var propertyInfos = contextType.GetProperties();
    foreach (var table in propertyInfos)
    {
        //I'm only worried about ITable<> now, otherwise skip it
        if ((!table.PropertyType.IsGenericType) ||
            table.PropertyType.GetGenericTypeDefinition() != typeof (ITable<>)) continue;

        //Determine the generic type of the ITable<>
        var TableType = GetTableType(table);
        //Create a List<T> of that type 
        var emptyList = CreateGeneric(typeof (List<>), TableType);
        //Create my InMemoryTable<T> of that type
        var inMemoryTable = CreateGeneric(typeof (InMemoryTable<>), TableType, emptyList);  

        //NOW SETUP MOCK TO RETURN THAT TABLE
        //How do I call instance.Setup(i=>i.THEPROPERTYNAME).Returns(inMemoryTable) ??
    }
return instance.Object;
}

到目前为止,我已经知道如何创建我需要设置Mock的对象,但我就是无法找出如何动态调用Moq的Setup()方法并传入属性名称。我开始研究反射来调用Moq的Setup()方法,但很快就变得非常丑陋。
有没有人有一个简单的方法来像这样动态调用Setup()和Returns()?
编辑:Brian的答案让我明白了。以下是它的工作原理:
public static DataContext GetMockContext<T>() where T: DataContext
    {
        Type contextType = typeof (T);
        var instance = new Mock<T>();
        var propertyInfos = contextType.GetProperties();
        foreach (var table in propertyInfos)
        {
            //I'm only worried about ITable<> now, otherwise skip it
            if ((!table.PropertyType.IsGenericType) ||
                table.PropertyType.GetGenericTypeDefinition() != typeof(ITable<>)) continue;

            //Determine the generic type of the ITable<>
            var TableType = GetTableType(table);
            //Create a List<T> of that type 
            var emptyList = CreateGeneric(typeof(List<>), TableType);
            //Create my InMemoryTable<T> of that type
            var inMemoryTable = CreateGeneric(typeof(InMemoryTable<>), TableType, emptyList);

            //NOW SETUP MOCK TO RETURN THAT TABLE
            var parameter = Expression.Parameter(contextType);
            var body = Expression.PropertyOrField(parameter, table.Name);
            var lambdaExpression = Expression.Lambda<Func<T, object>>(body, parameter); 

            instance.Setup(lambdaExpression).Returns(inMemoryTable);
        }
        return instance.Object;
    }

使用lambda方法的三行代码正在创建一个方法并调用它。您需要使用我的答案中的示例创建lambda方法并将其传递给设置方法。 - Brian Dishaw
感谢您的编辑。在第一段代码中,您提到了具体上下文(ContactsDataContext),并希望将其迁移到使用通用上下文(DataContext)。问题在于,您正在将 DataContext 的模拟与存在于模板类上的属性混合在一起。我将更新我的答案,并添加更多的代码。 - Brian Dishaw
啊,我想你的评论解决了我的问题。我需要在顶部创建一个Mock<T>而不是Mock<DataContext>。它可以工作了! - Jake Stevenson
1个回答

9
你需要的是Linq表达式。下面是构建属性访问器表达式的示例代码。
使用以下类:
public class ExampleClass
{
   public virtual string ExampleProperty
   {
      get;
      set;
   }

   public virtual List<object> ExampleListProperty
   {
      get;
      set;
   }
}

以下测试演示了使用 Linq.Expression 类动态访问其属性。
[TestClass]
public class UnitTest1
{
   [TestMethod]
   public void SetupDynamicStringProperty()
   {
      var dynamicMock = new Mock<ExampleClass>();

      //Class type
      var parameter = Expression.Parameter( typeof( ExampleClass ) );           
      
      //String rep of property
      var body = Expression.PropertyOrField( parameter, "ExampleProperty" ); 

      //build the lambda for the setup method
      var lambdaExpression = Expression.Lambda<Func<ExampleClass, object>>( body, parameter );

      dynamicMock.Setup( lambdaExpression ).Returns( "Works!" );

      Assert.AreEqual( "Works!", dynamicMock.Object.ExampleProperty );
   }

   [TestMethod]
   public void SetupDynamicListProperty_IntFirstInList()
   {
      var dynamicMock = new Mock<ExampleClass>();

      var parameter = Expression.Parameter( typeof( ExampleClass ) );
      var body = Expression.PropertyOrField( parameter, "ExampleListProperty" );
      var lambdaExpression = Expression.Lambda<Func<ExampleClass, object>>( body, parameter );

      var listOfItems = new List<object> { 1, "two", DateTime.MinValue };
      dynamicMock.Setup( lambdaExpression ).Returns( listOfItems );

      Assert.AreEqual( typeof( int ), dynamicMock.Object.ExampleListProperty[0].GetType() );
      Assert.AreEqual( 1, dynamicMock.Object.ExampleListProperty[0] );

      Assert.AreEqual( 3, dynamicMock.Object.ExampleListProperty.Count );
   }

   [TestMethod]
   public void SetupDynamicListProperty_StringSecondInList()
   {
      var dynamicMock = new Mock<ExampleClass>();

      var parameter = Expression.Parameter( typeof( ExampleClass ) );
      var body = Expression.PropertyOrField( parameter, "ExampleListProperty" );
      var lambdaExpression = Expression.Lambda<Func<ExampleClass, object>>( body, parameter );

      var listOfItems = new List<object> { 1, "two" };
      dynamicMock.Setup( lambdaExpression ).Returns( listOfItems );

      Assert.AreEqual( typeof( string ), dynamicMock.Object.ExampleListProperty[1].GetType() );
      Assert.AreEqual( "two", dynamicMock.Object.ExampleListProperty[1] );

      Assert.AreEqual( 2, dynamicMock.Object.ExampleListProperty.Count );
   }
}

编辑

您的代码已经走得太远了。该代码创建了一个与所需lambda签名相同的方法,然后执行它(.Invoke)。然后,您尝试将对象的结果(因此出现编译错误)传递到 Moq 的设置中。一旦告诉 Moq 如何操作(即 lambda 表达式),Moq 将为您执行方法执行和连接。如果使用我提供的 lambda 表达式创建方式,则可以构建所需内容。

var funcType = typeof (Func<>).MakeGenericType(new Type[] {TableType, typeof(object)});

var lambdaMethod = typeof (Expression).GetMethod("Lambda");
var lambdaGenericMethod = lambdaMethod.MakeGenericMethod(funcType);
var lambdaExpression = lambdaGenericMethod.Invoke(body, parameter);

//var lambdaExpression = Expression.Lambda<Func<ExampleClass, object>>(body, parameter); // FOR REFERENCE FROM BRIAN'S CODE
instance.Setup(lambdaExpression).Returns(inMemoryTable);

做这个代替

var parameter = Expression.Parameter( TableType );
var body = Expression.PropertyOrField( parameter, "PutYourPropertyHere" );
var lambdaExpression = Expression.Lambda<Func<ExampleClass, object>>( body, parameter );

instance.Setup(lambdaExpression).Returns(inMemoryTable);

编辑

尝试修正GetMockContext。请注意一些更改(我标记了每一行)。我认为这更接近正确。我想知道,InMemoryTable是否继承自DataContext?如果不是的话,该方法签名将不正确。

public static object GetMockContext<T>() where T: DataContext
{
    Type contextType = typeof (T);
    var instance = new Mock<T>();  //Updated this line
    var propertyInfos = contextType.GetProperties();
    foreach (var table in propertyInfos)
    {
        //I'm only worried about ITable<> now, otherwise skip it
        if ((!table.PropertyType.IsGenericType) ||
            table.PropertyType.GetGenericTypeDefinition() != typeof(ITable<>)) continue;

        //Determine the generic type of the ITable<>
        var TableType = GetTableType(table);
        //Create a List<T> of that type 
        var emptyList = CreateGeneric(typeof(List<>), TableType);
        //Create my InMemoryTable<T> of that type
        var inMemoryTable = CreateGeneric(typeof(InMemoryTable<>), TableType, emptyList);

        //NOW SETUP MOCK TO RETURN THAT TABLE
        var parameter = Expression.Parameter(contextType);
        var body = Expression.PropertyOrField(parameter, table.Name);
        var lambdaExpression = Expression.Lambda<Func<T, object>>(body, parameter); 

        instance.Setup(lambdaExpression).Returns(inMemoryTable);
    }
    return instance.Object; //had to change the method signature because the inMemoryTable is not of type DataContext. Unless InMemoryTable inherits from DataContext?
}

我希望这能够帮到你!

它让我更接近了解动态创建表达式,但如果我只知道反射中的类型,我仍然很困惑。请参见我对原始问题的编辑。 - Jake Stevenson
尝试将lambda函数的func定义为类似于Func<dynamic, object>的形式。我认为这样做会使你达到预期的目标。假设使用的是 .net 4.0... - Brian Dishaw
然后我得到了一个错误:无法将“System.Linq.Expressions.Expression<System.Func<dynamic,object>>”转换为“System.Linq.Expressions.Expression<System.Action<System.Data.Linq.DataContext>>”。 - Jake Stevenson
你最新示例的问题在于你使用了 Func<ExampleClass, object>,但是我需要在运行时确定“ExampleClass”(作为 TableType 变量传递)。 - Jake Stevenson
我再次进行编辑 - 这次是将其变为通用方法,并尝试使用它来构建lambda表达式。 - Jake Stevenson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接