如何向Mock DbSet(使用Moq)添加项目

79
我正在尝试为测试目的设置模拟DbSet。我使用了这里的教程http://www.loganfranken.com/blog/517/mocking-dbset-queries-in-ef6/,并稍微修改了一下,使得每次调用GetEnumerator返回一个新的枚举器(我遇到的另一个问题)。然而,我在向DbSet添加项目时遇到困难。
输出结果是preCount = 3 postCount = 3。但是,我希望它是precount = 3 postCount = 4。非常感谢您的任何帮助。
static void Main(string[] args)
    {
        Debug.WriteLine("hello debug");

        List<string> stringList = new List<string>
        {
            "a", "b", "c"
        };

        DbSet<string> myDbSet = GetQueryableMockDbSet(stringList);
        int preCount = myDbSet.Count();
        myDbSet.Add("d");
        int postCount = myDbSet.Count();
        Debug.WriteLine("preCount = " + preCount + " postCount = " + postCount);
    }

    private static DbSet<T> GetQueryableMockDbSet<T>(List<T> sourceList) where T : class
    {
        var queryable = sourceList.AsQueryable();

        var dbSet = new Mock<DbSet<T>>();
        dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
        dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
        dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
        dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());

        return dbSet.Object;
    }

非常好的封装创建dbSet的方法。 您有更新支持异步查询的可能性吗? - Paul Gorbas
1
至少目前来看,使用 .net core 1.0 可以解决异步问题:如何使用 Entity Framework Core 模拟异步存储库 - Paul Gorbas
2个回答

165

myDbSet 不是真正的 DbSet 实现,而是一个模拟,也就是说它是 假的,需要为你需要使用的所有方法进行设置。 Add 方法也不例外,因此需要设置它来执行你需要的操作,否则它什么也不会做。

添加类似下面的代码,当调用 myDbSet.Add("d"); 时,'d' 将被添加到列表中,并可以在以后返回。

dbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>((s) => sourceList.Add(s));

完整代码

private static DbSet<T> GetQueryableMockDbSet<T>(List<T> sourceList) where T : class
{
    var queryable = sourceList.AsQueryable();

    var dbSet = new Mock<DbSet<T>>();
    dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
    dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
    dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
    dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
    dbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>((s) => sourceList.Add(s));

    return dbSet.Object;
}

输出

hello debug
preCount = 3 postCount = 4

问题在于,当您向数据库添加内容时,您必须手动设置导航属性,而这种模拟将保存对象本身。我想知道是否有一种方法来模拟EF关于此的行为。 - tocqueville
4
嘲笑EF的意义在于不希望将数据库引入测试中。最好拥有运行快速、分区明确的测试,只测试特定的API。另一个选择是使用https://msdn.microsoft.com/en-us/data/dn314431.aspx "内存双倍体",但是内存双倍体和模拟本质上是相同的。 - andrew
1
你有没有更新支持异步查询的代码呢? - Paul Gorbas
至少目前使用 .net core 1.0,这将解决异步问题:如何使用 Entity Framework Core 模拟异步存储库 - Paul Gorbas
5
如果我没有看到这篇帖子和答案的话,我可能会永远失去 >.< 非常感谢,感谢提问者和回答者! - zeroflaw
显示剩余2条评论

3
要处理.Find(),我们可以类似地使用反射,但需要一些关于使用Find时的约定假设。
(希望这不会超出范围,我将此Q/A加为书签已有多年,并正在寻找一个Find实现...)
实现另一个助手如下:
static object find(IEnumerable<object> oEnumerable, object[] keys)        
{
    // assumptions: primary key of object is named ID
    // primary key of object is an int
    // keys passed to .Find() method is a single value of int type
    foreach (var o in oEnumerable)
    {
        var t = o.GetType();
        var prop = t.GetProperty("ID");
        if (prop != null)
        {
            if (prop.PropertyType == typeof(int))
            {
                if ((int)prop.GetValue(o) == (int)keys[0])
                {
                    return o;
                }
            }
        }                
    }
    return null;
}

而在 Daniel 提供的模拟设置示例中:

dbSet.Setup(d => d.Find(It.IsAny<object[]>())).Returns((object[] oArray) => find(sourceList, oArray) as T);

因为我们通常没有办法(或者不想)按照EF的方式来确定主键,所以我假设“ID”是关键字段的名称(这符合我的约定),但可以进一步扩展以接受多个变化。我还假设只有一个整数传递给Find(这是我的标准约定),但是也可以扩展以支持更强大的功能。

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