0

I've got an MVC ASP.Net app using Entity Framework v6.0 with an Employee's table.

We're using a Code First approach with the standard Create (CRUD) method that has a EF lookup for existing employee's and also an EF lookup for employee CreatedBy/ModifiedBy fields. Trying to create mocks for both (EF object stubs) fails.

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "EmployeeID,TeamID,EmployeeADID,FirstName,MiddleName,LastName,EmailAddress,IsAdministrator,IsDeleted")] Employee employee)
{
    if (ModelState.IsValid)
    {
        //Only unique employee IDs
        var existingEmployee = db.Employees.FirstOrDefault(o => o.EmployeeADID == employee.EmployeeADID);
        if(existingEmployee != null)
        {
            ViewBag.Error = "Employee ID must be unique, this employee (" + existingEmployee.FullName + ") already exists in the system.";
            ViewBag.TeamID = new SelectList(db.Teams, "TeamID", "Name", employee.TeamID);
            return View(existingEmployee);
        }

        SetAuditFields(employee);
        db.Employees.Add(employee);
        db.SaveChanges();
        return RedirectToAction("Index");
    }    
    ViewBag.TeamID = new SelectList(db.Teams, "TeamID", "Name", employee.TeamID);
    return View(employee);
}

The problem is the SetAuditFields call and I need to mock db.Employees.AsNoTracking AsNoTracking for the Edit operation.

private void SetAuditFields(Employee employee, bool onlyModified = false)
{
    char sep = '\\';
    string pID = User.Identity.Name.Split(sep)[1].ToUpper();
    var users = db.Employees.AsNoTracking().Where(c => c.EmployeeADID == pID || c.EmployeeID == employee.EmployeeID).ToList();
    var currentUser = users.FirstOrDefault(u => u.EmployeeADID == pID);
    if (onlyModified)
    {
        //Notice the AsNoTracking, when you set the db.Entry(object).State = EntityState.Modified; this query wont return anything as its in Modified Mode.
        //var originalEmployee = db.Employees.AsNoTracking().FirstOrDefault(o => o.EmployeeID == employee.EmployeeID);
        var originalEmployee = users.FirstOrDefault(o => o.EmployeeID == employee.EmployeeID);
        employee.CreatedByID = originalEmployee.CreatedByID;
        employee.CreatedDate = originalEmployee.CreatedDate;
        employee.ModifiedByID = currentUser.EmployeeID;
        employee.ModifiedDate = DateTime.Now;
    }
    else
    {
        employee.CreatedByID = currentUser.EmployeeID;
        employee.CreatedDate = DateTime.Now;
        employee.ModifiedByID = currentUser.EmployeeID;
        employee.ModifiedDate = DateTime.Now;
    }
}

So how do I mock db.Employees.AsNoTracking after initially mocking db.Employees?

The THESE TWO LINES COMMENTED OUT in the code below don't work and fail with:

"The member 'IQueryable.Provider' has not been implemented on type 'DbSet1Proxy' which inherits from 'DbSet1'

I also tried the mockContext.SetupSequence but I need to interchange between with AsNoTracking on and off. Surely there must be something I'm missing?

[TestMethod()]
public void Create_Submit_Test()
{
    // Arrange
    var employeeDoesntExist = new Employee { EmployeeID = 0, FirstName = "DoesntExist" };
    var employeeAdmin = new Employee { EmployeeID=140, FirstName = "Bern", MiddleName = "", LastName = "O", EmployeeADID = "123", EmailAddress = "Bernard.O@a.com", TeamID = 1, IsDeleted = false, IsAdministrator = true };
    var employeeNew = new Employee { FirstName = "Jez", MiddleName = "", LastName = "T", EmployeeADID = "321", EmailAddress = "Jeremy.Thompson@a.com", TeamID = 1, IsDeleted = false, IsAdministrator = true };

    var mockContext = new Mock<ICTEntities>();
    var employeeEmptyMock = base.GetQueryableMockDbSet(employeeDoesntExist);
    var employeeAdminMock = base.GetQueryableMockDbSet(employeeAdmin);

    //THESE TWO LINES COMMENTED OUT
    //mockContext.Setup(m => m.Employees).Returns(employeeEmptyMock);
    //mockContext.Setup(m => m.Employees.AsNoTracking()).Returns(employeeAdminMock);

    mockContext.SetupSequence(x => x.Employees.AsNoTracking())
    .Returns(employeeEmptyMock)
    .Returns(employeeAdminMock);

    //I dont want to save it to the Database, otherwise next time we run this the object will already exist, so I mock the call
    mockContext.Setup(d => d.SaveChanges()).Returns(1);

    var controller = new EmployeesController(mockContext.Object);
    controller.ControllerContext = base.MockAccess().Object;

    // Act
    RedirectToRouteResult result = controller.Create(employeeNew) as RedirectToRouteResult;

    // Assert
    Assert.IsNotNull(result);
    Assert.AreEqual("Index", result.RouteValues["Action"]); 
}

Here is the GetQueryableMockDbSet method:

protected DbSet<T> GetQueryableMockDbSet<T>(params 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;
}
halfer
  • 19,824
  • 17
  • 99
  • 186
Jeremy Thompson
  • 61,933
  • 36
  • 195
  • 321
  • Have a look at the way this (https://stackoverflow.com/questions/41324783/unit-testing-tolistasync-using-an-in-memory/41327224) example set up the mocks, as well as the fix to the issue they had around async queries as well. (since that will quite possibly come up.) They configured the mocks a bit differently. Personally I mock with a repository pattern as the cut-off, but used that DbAsyncEnumerable pattern to wrap the stub data. – Steve Py Mar 26 '19 at 05:20
  • @StevePy thanks, I've been through that, its actually from [Using Mocking to create a In Memory DB](https://docs.microsoft.com/en-gb/ef/ef6/fundamentals/testing/mocking), and its sister article [Manually creating a InMemory dB](https://docs.microsoft.com/en-gb/ef/ef6/fundamentals/testing/writing-test-doubles). I dont want to really get into the philosophy around a unit test being isolated otherwise its an integration test. Typically I follow a Repository/Storage pattern but this time I've just decided to mock the DB. I'm almost there, I've got 32 tests passing and this single 1 failing. – Jeremy Thompson Mar 26 '19 at 05:30
  • `AsNoTracing` is an extension method. Mock it on `dbSet`... – Johnny Mar 26 '19 at 06:05
  • @Johnny how do I do that? *I usually use NSubstitute*. – Jeremy Thompson Mar 26 '19 at 06:19
  • @JeremyThompson why not just have all the objects/entities in one collection? The `Where` should filter out what does not match the provided predicate. Note that [`AsNoTracking`](https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.dbextensions.asnotracking#System_Data_Entity_DbExtensions_AsNoTracking__1_System_Linq_IQueryable___0__) If the underlying query object does not have a AsNoTracking method, then calling this method will have no affect. – Nkosi Mar 26 '19 at 10:46
  • 1
    @JeremyThompson did you consider Effort (https://entityframework-effort.net/) for unit testing your EF Code? In our projects we learned that effort is much easier to use compared to mocking DbSets ourself. – Andre Kraemer Mar 26 '19 at 22:44
  • @Nkosi I did what you said to get the Edit Submit to be testable. I'm thinking there has to be an easy way to chain mocks together as in a queue. Do you have any other ideas? Thank everyone!! – Jeremy Thompson Mar 27 '19 at 06:10
  • @JeremyThompson as far as I am aware, that is how things are with Moq and EF. – Nkosi Mar 27 '19 at 06:13
  • My thoughts on this is you are trying to fit what is really integration testing into your unit test. You can try out using Sqlite or SqlCE instead as double to your full blown RDBMS in your testing. You'll see results closer to how your application will run. Found this link that may help - https://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort – Migg Mar 27 '19 at 07:37

1 Answers1

1

I ended up creating a chain of mocks using a counter as per https://stackoverflow.com/a/14368486/495455

int callCounter = 1;
mockContext.Setup(m => m.Employees)
    .Returns(() =>
    {
        if (callCounter == 1)
        {
            callCounter++;
            return employeeToEditMockCU;
        }
        else
        {
            return employeeMockCU;
        }
    });

Mocking using a SetupSequence doesn't work for me after the first mock. The db.Employee becomes null after the first call. So I don't use a SetupSequence:

mockContext.SetupSequence(x => x.Employees)
.Returns(employeeToEditMockCU)
.Returns(employeeMockCU);

Also to get around the AsNoTracking() I ended up fetching the record to update and saving it without using EntityState.Modified:

EF Update using EntityState.Modified
How to update record using Entity Framework 6?

halfer
  • 19,824
  • 17
  • 99
  • 186
Jeremy Thompson
  • 61,933
  • 36
  • 195
  • 321