Unit Testing EF Data With Moq

Introduction

I’ve discussed using the in-memory Entity Framework unit tests in a previous post (here).  In this post, I’m going to demonstrate a simple way to use Moq to unit test a method that uses Entity Framework Core.

Setup

For this sample, I used the POCOs, context and config files from this project (here).  You can copy the cs files from that project, or you can just download the sample project from GitHub at the end of this article.

You’ll need several parts to make your unit tests work:

  1. IOC container – Not in this post
  2. List object to DbSet Moq method
  3. Test data
  4. Context Interface

I found a method on stack overflow (here) that I use everywhere.  I created a unit test helper static object and placed it in my unit test project:

public class UnitTestHelpers
{
  public 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>(sourceList.Add);

    return dbSet.Object;
  }
}

The next piece is the pretend data that you will use to test your method.  You’ll want to keep this as simple as possible.  In my implementation, I allow for multiple data sets.

public static class ProductTestData
{
  public static List Get(int dataSetNumber)
  {
    switch (dataSetNumber)
    {
      case 1:
      return new List
      {
        new Product
        {
          Id=0,
          Store = 1,
          Name = "Cheese",
          Price = 2.5m
        },
        ...

      };
    }
    return null;
  }
}

Now you can setup a unit test and use Moq to create a mock up of your data and then call your method under test.  First, let’s take a look at the method and see what we want to test:

public class ProductList
{
  private readonly IDatabaseContext _databaseContext;

  public ProductList(IDatabaseContext databaseContext)
  {
    _databaseContext = databaseContext;
  }

  public List GetTopTen()
  {
    var result = (from p in _databaseContext.Products select p).Take(10).ToList();

    return result;
  }
}

The ProductList class will be setup from an IOC container.  It has a dependency on the databaseContext object.  That object will be injected by the IOC container using the class constructor.  In my sample code, I set up the class for this standard pattern.  For unit testing purposes, we don’t need the IOC container, we’ll just inject our mocked up context into the class when we create an instance of the object.

Let’s mock the context:

[Fact]
public void TopTenProductList()
{
  var demoDataContext = new Mock<IDatabaseContext>();

}

As you can see, Moq uses interfaces to create a mocked object.  This is the only line of code we need for the context mocking.  Next, we’ll mock some data.  We’re going to tell Moq to return data set 1 if the Products getter is called:

[Fact]
public void TopTenProductList()
{
  var demoDataContext = new Mock<IDatabaseContext>();
  demoDataContext.Setup(x => x.Products).Returns(UnitTestHelpers.GetQueryableMockDbSet(ProductTestData.Get(1)));

}

I’m using the GetQueryableMockDbSet unit test helper method in order to convert my list objects into the required DbSet object.  Any time my method tries to read Products from the context, data set 1 will be returned.  This data set contains 12 items.  As you can see from the method that we are going to mock up, there should be only ten items returned.  Let’s add the method under test setup:

[Fact]
public void TopTenProductList()
{
  var demoDataContext = new Mock<IDatabaseContext>();
  demoDataContext.Setup(x => x.Products).Returns(UnitTestHelpers.GetQueryableMockDbSet(ProductTestData.Get(1)));

  var productList = new ProductList(demoDataContext.Object);

  var result = productList.GetTopTen();
  Assert.Equal(10,result.Count);
}

The object under test is very basic, just get an instance and pass the mocked context (you have to use .Object to get the mocked object).  Next, just call the method to test.  Finally, perform an assert to conclude your unit test.  If the productList() method returns an amount that is not ten, then there is an issue (for this data set).  Now, we should test an empty set.  Add this to the test data switch statement:

case 2:
  return new List
  {
  };

Now the unit test:

[Fact]
public void TopTenProductList()
{
  var demoDataContext = new Mock<IDatabaseContext>();
  demoDataContext.Setup(x => x.Products).Returns(UnitTestHelpers.GetQueryableMockDbSet(ProductTestData.Get(2)));

  var productList = new ProductList(demoDataContext.Object);

  var result = productList.GetTopTen();
  Assert.Empty(result.Count);
}

All the work has been done to setup the static test data object, so I only had to add one case to it.  Then the unit test is identical to the previous unit test, except the ProductTestData.Get() has a parameter of 2, instead of 1 representing the data set number.  Finally, I changed the assert to test for an empty set instead of 10.  Execute the tests:

Now you can continue to add unit tests to test for different scenarios.

Where to Get the Code

You can go to my GitHub account and download the sample code (click here).  If you would like to create the sample tables to make this program work (you’ll need to add your own console app to call the GetTopTen() method), you can use the following MS SQL Server script:

CREATE TABLE [dbo].[Store](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Name] [varchar](50) NULL,
	[Address] [varchar](50) NULL,
	[State] [varchar](50) NULL,
	[Zip] [varchar](50) NULL,
 CONSTRAINT [PK_Store] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[Product](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Store] [int] NOT NULL,
	[Name] [varchar](50) NULL,
	[Price] [money] NULL,
 CONSTRAINT [PK_Product] PRIMARY KEY NONCLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

SET ANSI_PADDING OFF
GO

ALTER TABLE [dbo].[Product]  WITH CHECK ADD  CONSTRAINT [FK_store_product] FOREIGN KEY([Store])
REFERENCES [dbo].[Store] ([Id])
GO

ALTER TABLE [dbo].[Product] CHECK CONSTRAINT [FK_store_product]
GO
 

Leave a Reply