Reputation: 2358
I am trying to test an update function by mocking the data using Moq. I am using Entity Framework 6.
I can print out a count of the DbSet
and it is the expected amount. However, when it tries to select an object, it throws an exception, NullReferenceException: Object reference not set to an instance of an object.
Here is my test class which sets up the mocked DbSets
and DbContext
[TestFixture]
public class ProductControllerTest
{
private ProductController controller;
private IProductRepository productRepo;
private IUnitOfWork unitOfWork;
private IBrandRepository brandRepo;
private ICategoryRepository categoryRepo;
private ISegmentRepository segmentRepo;
private ITypeRepository typeRepo;
private IEnumerable<Product> productList;
[SetUp]
public void Init()
{
IEnumerable<Brand> brandList = new List<Brand>{
new Brand{
Id = 1,
Name = "Unknown"
},
new Brand{
Id = 2,
Name = "Clorox"
},
new Brand{
Id = 3,
Name = "Glad"
}
};
var brandData = brandList.AsQueryable();
productList = new List<Product>{
new Product{
Id = "0000000001",
ParentAsin = "0000000010",
Title = "Mocked Product #1",
ReleaseDate = DateTime.Now,
BrandId = 1,
CategoryId = 1,
SegmentId = 1,
TypeId = 1,
Brand = brandList.ElementAt(0)
},
new Product{
Id = "0000000002",
ParentAsin = "0000000010",
Title = "Mocked Product #2",
ReleaseDate = DateTime.Now,
BrandId = 1,
CategoryId = 1,
SegmentId = 1,
TypeId = 1,
Brand = brandList.ElementAt(0)
},
new Product{
Id = "0000000003",
ParentAsin = "0000000010",
Title = "Mocked Product #3",
ReleaseDate = DateTime.Now,
BrandId = 2,
CategoryId = 3,
SegmentId = 3,
TypeId = 2,
Brand = brandList.ElementAt(1)
}
};
var productData = productList.AsQueryable();
brandList.ElementAt(1).Products.Add(productList.ElementAt<Product>(2));
var mockProductSet = new Mock<DbSet<Product>>();
mockProductSet.As<IQueryable<Product>>().Setup(m => m.Provider).Returns(productData.Provider);
mockProductSet.As<IQueryable<Product>>().Setup(m => m.Expression).Returns(productData.Expression);
mockProductSet.As<IQueryable<Product>>().Setup(m => m.ElementType).Returns(productData.ElementType);
mockProductSet.As<IQueryable<Product>>().Setup(m => m.GetEnumerator()).Returns(productData.GetEnumerator());
var mockBrandSet = new Mock<DbSet<Brand>>();
mockBrandSet.As<IQueryable<Brand>>().Setup(m => m.Provider).Returns(brandData.Provider);
mockBrandSet.As<IQueryable<Brand>>().Setup(m => m.Expression).Returns(brandData.Expression);
mockBrandSet.As<IQueryable<Brand>>().Setup(m => m.ElementType).Returns(brandData.ElementType);
mockBrandSet.As<IQueryable<Brand>>().Setup(m => m.GetEnumerator()).Returns(brandData.GetEnumerator());
var mockContext = new Mock<ApplicationDbContext>() { CallBase = true };
mockContext.Setup(m => m.Set<Product>()).Returns(mockProductSet.Object);
mockContext.Setup(m => m.Set<Brand>()).Returns(mockBrandSet.Object);
unitOfWork = new UnitOfWork(mockContext.Object);
brandRepo = new BrandRepository(mockContext.Object);
productRepo = new ProductRepository(mockContext.Object);
controller = new ProductController(productRepo, unitOfWork, brandRepo, categoryRepo, segmentRepo, typeRepo);
}
[Test]
public void TestReturnEditedModel()
{
Product product = productList.ElementAt<Product>(1);
product.BrandId = 3;
product.CategoryId = 2;
product.SegmentId = 2;
product.TypeId = 3;
controller.Edit(product, "Return value");
Product result = productRepo.Get(product.Id);
Assert.AreEqual(product.Id, result.Id);
Assert.AreEqual(3, result.BrandId);
Assert.AreEqual(2, result.CategoryId);
Assert.AreEqual(2, result.SegmentId);
Assert.AreEqual(3, result.TypeId);
}
}
I provided only the test that is failing.
Here is the controller function being called
[HttpPost]
public ActionResult Edit([Bind(Include = "Id,Title,ParentAsin,ReleaseDate,BrandId,CategoryId,SegmentId,TypeId")]Product model, string returnAction)
{
if(!ModelState.IsValid)
{
Dictionary<string, int> selectedIds = new Dictionary<string, int>();
selectedIds.Add("BrandId", model.BrandId);
selectedIds.Add("CategoryId", model.CategoryId);
selectedIds.Add("SegmentId", model.SegmentId);
selectedIds.Add("TypeId", model.TypeId);
PopulateAllDropDownLists(selectedIds);
return View(model);
}
model.Brand = _brandRepo.Get(model.BrandId);
model.Category = _categoryRepo.Get(model.CategoryId);
model.Segment = _segmentRepo.Get(model.SegmentId);
model.Type = _typeRepo.Get(model.TypeId);
_repository.Update(model);
_unitOfWork.SaveChanges();
return RedirectToAction(returnAction);
}
_brandRepo
is of type IBrandRepository
and after all the implementations and inheritance, the function Get()
is in a Generic repository class.
Here is the Get function that is being called.
public virtual TEntity Get(TId id)
{
return this.DbSet.Single(x => (object)x.Id == (object)id);
}
The return line is the what is throwing the error.
Since this is a test and I'm mocking the data, I know that the Id being passed in is correct. It starts off as an int
and the Id
of Brand
is an int
as well, but to make this generic, the property is of type TId
is a generic type of the interface TEntity
which all models implement.
Here is TEntity
public interface IEntity<TId>
{
/// <summary>
/// Gets or sets the unique identifier.
/// </summary>
/// <value>The unique identifier.</value>
TId Id { get; set; }
}
I'm not sure if this is a mocking issue or an issue with using generic types. Can someone help with this.
Upvotes: 2
Views: 1239
Reputation: 131
That looks way too complicated; you could just use a generic method to create a mock for any DbSet...
public static class DbSetMock
{
public static Mock<DbSet<T>> CreateFrom<T>(List<T> list) where T : class
{
var internalQueryable = list.AsQueryable();
var mock = new Mock<DbSet<T>>();
mock.As<IQueryable<T>>().Setup(x => x.Provider).Returns(internalQueryable.Provider);
mock.As<IQueryable<T>>().Setup(x => x.Expression).Returns(internalQueryable.Expression);
mock.As<IQueryable<T>>().Setup(x => x.ElementType).Returns(internalQueryable.ElementType);
mock.As<IQueryable<T>>().Setup(x => x.GetEnumerator()).Returns(()=> internalQueryable.GetEnumerator());
mock.As<IDbSet<T>>().Setup(x => x.Add(It.IsAny<T>())).Callback<T>(element => list.Add(element));
mock.As<IDbSet<T>>().Setup(x => x.Remove(It.IsAny<T>())).Callback<T>(element => list.Remove(element));
return mock;
}
}
Then you can use it like:
var mockBrandSet = DbSetMock.CreateFrom(brandList);
Since the data inside of the list and the DbSet are the same you can check the list to assert your manipulations.
Upvotes: 3
Reputation: 246998
If at any point you try to access the DbSets via their properties and not via Set<>
is what would cause that problem if they were not setup. Although call base was true in the original example, the DbContext
would internally try to discover DbSets
and initializes them which would fail when mocking the DbContext. This is what they would have to setup in the mock to override the default behavior.
var mockContext = new Mock<ApplicationDbContext>();
mockContext.Setup(m => m.Set<Product>()).Returns(mockProductSet.Object);
mockContext.Setup(m => m.Set<Brand>()).Returns(mockBrandSet.Object);
mockContext.Setup(m => m.Products).Returns(mockProductSet.Object);
mockContext.Setup(m => m.Brands).Returns(mockBrandSet.Object);
Also when setting up the GetEnumerator()
, use a function to allow for multiple call
eg
mockProductSet.As<IQueryable<Product>>()
.Setup(m => m.GetEnumerator())
.Returns(() => productData.GetEnumerator());
Upvotes: 0