在UnitTest上使用Mock-DbContext

时间:2014-07-31 14:13:09

标签: c# asp.net-mvc entity-framework unit-testing mocking

在我的C#项目中,我最后有一个.ToList();的查询:

public List<Product> ListAllProducts()
{
    List<Product> products = db.Products
        .Where(...)
        .Select(p => new Product()
        {
            ProductId = p.ProductId,
            ...
        })
        .ToList();

    return products;
}

我不时收到EntityException: "The underlying provider failed on Open."错误。我根据我在SO

上找到的答案做了以下工作
public List<Product> ListAllProducts()
{
    try
    {
        // When accessing a lot of Database Rows the following error might occur:
        // EntityException: "The underlying provider failed on Open." With an inner TimeoutExpiredException.
        // So, we use a new db so it's less likely to occur
        using(MyDbContext usingDb = new MyDbContext())
        {
            // and also set the CommandTimeout to 2 min, instead of the default 30 sec, just in case
            ((IObjectContextAdapter)usingDb).ObjectContext.CommandTimeout = 120;

            List<Product> products = usingDb.Products
                .Where(...)
                .Select(p => new Product()
                {
                    ProductId = p.ProductId,
                    ...
                })
                .ToList();

            return products;
        }
    }
    // and also have a try-catch in case the error stil occurres,
    // since we don't want the entire website to crash
    catch (EntityException ex)
    {
        Console.WriteLine("An error has occured: " + ex.StackTrace);
        return new List<Product>();
    }
}

到目前为止它似乎工作,我每次都收到我的数据(以防万一我也添加了try-catch)。

现在我是UnitTesting,我正面临着一个问题。我的UnitTest中有一个MockDbContext,但由于我使用了使用非模拟版本的using(...),因此我的UnitTests失败了。我怎么能用MockDbContext而不是默认的测试呢?


编辑:

我刚遇到this post about UnitTesting Mock DbContext。所以在我看来,我确实需要将它一直传递给我的方法,或者只是不使用using(MyDbContext usingDb = new MyDbContext())而只是暂时增加CommandTimeout并使用try-catch。真的没有别的办法吗?或者,如果没有using(MyDbContext usingDb = new MyDbContext())所有错误,我是否有更好的修复/解决方法?


编辑2:

让我的情况更加清晰:

我有MyDbContext-class和IMyDbContext-interface。

我的每个控制器都有以下内容:

public class ProductController : Controller
{
    private IMyDbContext _db;

    public ProductController(IMyDbContext db)
    {
        this._db = db;
    }

    ... // Methods that use this "_db"

    public List<Product> ListAllProducts()
    {
        try
        {
            using(MyDbContext usingDb = new MyDbContext())
            {
                ... // Query that uses "usingDb"
            }
        }
        catch(...)
        {
            ...
        }
    }
}

在我的正常代码中,我创建了这样的实例:

ProductController controller = new ProductController(new MyDbContext());

在我的UnitTest代码中,我创建了这样的实例:

ProductController controller = new ProductController(this.GetTestDb());

// With GetTestDb something like this:
private IMyDbContext GetTestDb()
{
    var memoryProducts = new Product { ... };
    ...

    var mockDB = new Mock<FakeMyDbContext>();
    mockDb.Setup(m => m.Products).Returns(memoryProducts);
    ...
    return mockDB.Object;
}

一切都很好,在我的正常项目中作为我的UnitTest,除了ListAll方法中的这个using(MyDbContext usingDb = new MyDbContext())部分。

2 个答案:

答案 0 :(得分:3)

虽然希望对代码进行因子分解以便于测试,但是只有在测试中执行的代码分支并不起作用。实际上它的作用是将测试环境配置代码放入您的实时代码中。

您需要的是创建DbContext的另一个抽象点。您应该传入构造DbContext

的委托
public List<Product> ListAllProducts(Func<DbContext> dbFunc)
{
    using(MyDbContext usingDb = dbFunc())
    {
        // ...
    }
}

现在您不再传递繁重的DbContext对象,可以将方法参数重新计算到类构造函数中。

public class ProductDAL
{
    private Func<DbContext> _dbFunc;

    public ProductDAL(Func<DbContext> dbFunc)
    {
        _dbFunc = dbFunc;
    }

    public List<Product> ListAllProducts()
    {
        using(MyDbContext usingDb = _dbFunc())
        {
            // ...
        }
    }
}

最重要的是,您现在可以控制使用哪个DbContext,包括从配置文件中提取的连接字符串,一直在您的IOC容器中,并且永远不会担心它在其他任何地方代码。

答案 1 :(得分:0)

根据我的经验,如果不将代码结构化为单元测试友好,很难得到正确的单元测试,但是你可以尝试使该类具有通用性,以便可以使用任一上下文进行实例化。

public class SomeClass<T> where T: MyDbContext, new()
{
    public List<Product> ListAllProducts()
    {
        using(MyDbContext usingDb = new T())
        {
            ((IObjectContextAdapter)usingDb).ObjectContext.CommandTimeout = 120;
            List<Product> products = usingDb.Products.ToList();
            return products;
        }
    }
}


// real-code:
var foo = new SomeClass<MyDbContext>();

// test
var bar = new SomeClass<MockContext>();