Kevin Cruijssen
Kevin Cruijssen

Reputation: 9336

Using on UnitTest Mock-DbContext

In my C# Project I had a query with a .ToList(); at the end:

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

    return products;
}

From time to time, I was getting a EntityException: "The underlying provider failed on Open." error. I made the following work-around based on answers I found here on 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>();
    }
}

So far it seems to work and I receive my data every time (and just in case I also added the try-catch).

Now I'm UnitTesting and I'm facing a problem. I have a MockDbContext in my UnitTest, but since I'm using the using(...) which uses the non-Mock version, my UnitTests fail. How am I able to test this using the MockDbContext instead of the default one?


EDIT:

I just came across this post about UnitTesting Mock DbContext. So it seems to me I do indeed need to pass it all the way to my method, or just don't use the using(MyDbContext usingDb = new MyDbContext()) and instead only temporarily increase the CommandTimeout and use the try-catch. Is there really no other way? Or is there a better fix / workaround for the error I'm getting, without the using(MyDbContext usingDb = new MyDbContext()) all together?


EDIT 2:

To make my situation more clear:

I have MyDbContext-class and a IMyDbContext-interface.

I have the following in each of my Controllers:

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(...)
        {
            ...
        }
    }
}

In my normal code I create the instance like this:

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

In my UnitTest code I create the instance like this:

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;
}

Everything works great, both in my normal project as my UnitTest, except for this using(MyDbContext usingDb = new MyDbContext()) part in the ListAll-methods.

Upvotes: 2

Views: 1112

Answers (2)

Kenned
Kenned

Reputation: 578

In my experience it is difficult to get proper unit testing without structuring the code to be unit-test-friendly, but you could try making the class generic so it could be instantiated with either context.

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>();

Upvotes: 0

Keith Payne
Keith Payne

Reputation: 3082

While it is desirable to factor code to facilitate testing, having code branches that are only executed within a test doesn't help. Really what this does is place test environment configuration code into your live code.

What's you need is one more bit of abstraction for the creation of the DbContext. You should pass in a delegate that constructs the DbContext:

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

And now that you are no longer passing around a heavy DbContext object, you can re-factor the method parameter into the class constructor.

public class ProductDAL
{
    private Func<DbContext> _dbFunc;

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

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

And the cherry on top is that you can now control which DbContext to use, including the connection string pulled from a config file, all the way out in your IOC container and never worry about it anywhere else in the code.

Upvotes: 2

Related Questions