Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DbContext.ChangeTracker in automated tests throws SQLException

I am writing automated tests for a project I've been working on to become more familiar with MVC, EntityFramework (code first), unit testing, and Moq.

I have a section in my Repository class which sets a LastModified field of my models whenever Repository.SaveChanges() is called by the controller that works like this (MyModelBase is a base class):

public void RepoSaveChanges()
{
    foreach(var entry in _dbContext.ChangeTracker.Entities().Where(e => e.State == EntityState.Modified))
    {
        MyModelBase model = entry.Entity as MyModelBase;
        if (model != null)
        {
            model.LastModified = DateTime.Now;
        }
    }
    _dbContext.SaveChanges();
}

This works fine during application run time in the normal online environment, but it breaks when running in test methods. I use Moq to mock the DbSets in the DbContext and set up my test data.

Here's the weird part for me: My unit tests run fine (pass) but they don't ever actually enter the foreach loop - it hangs when ChangeTracker.Entities() is accessed and quits the loop, jumping down to _dbContext.SaveChanges(). There is no error.

However, on a friend's machine who shares the project with me, he gets an SQLException when ChangeTracker.Entities() is accessed. I do have SQLExceptions checked as thrown in VS2015 and there is no output or other indication of an exception on my side.

Result StackTrace:
at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection) ....

Result Message:
Test method MyProject.Tests.TestClasses.MyControllerTests.TestCreate threw exception: System.Data.SqlClient.SqlException: A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified)

Finally, my question is: is there a way to use Moq to mock the ChangeTracker (I suspect not from prior investigation), or is there another approach I can take to my RepoSaveChanges() that automatically sets a property? Without accessing ChangeTracker.Entities() I would need to have update logic to set the LastModified field for every model type that has it. Similarly, I feel like avoiding the use of that API/part of the framework because testing is being stubborn is not ideal.

Does anyone have any insight as to why the SQLException is not thrown/cannot be caught on my machine? Or any suggestions on how to use ChangeTracker.Entities() in unit tests? I would only set the LastModified property individually across all my models and controllers as a last resort.

Update: More sample code has been requested so let me go into further detail. I mock a DbContext using moq, then mock the DbSet objects contained in the DbContext:

var mockContext = new Mock<MyApplicationDbContext>();   //MyApplicationDbContext extends DbContext

Person p = new Person();
p.Name = "Bob";
p.Employer = "Superstore";

List<Person> list = new List<Person>();
list.Add(p);

var queryable = list.AsQueryable();

Mock<DbSet<Person>> mockPersonSet = new Mock<DbSet<Person>>();
mockPersonSet.As<IQueryable<Person>>().Setup(set => set.Provider).Returns(queryable.Provider);
mockPersonSet.As<IQueryable<Person>>().Setup(set => set.Expression).Returns(queryable.Expression);
mockPersonSet.As<IQueryable<Person>>().Setup(set => set.ElementType).Returns(queryable.ElementType);
mockPersonSet.As<IQueryable<Person>>().Setup(set => set.GetEnumerator()).Returns(() => queryable.GetEnumerator()); 

DbSet<Person> dbSet = mockPersonSet.Object as DbSet<Person>;
mockPersonSet.Setup(set => set.Local).Returns(dbSet.Local);

mockContext.Setup(context => context.Set<Person>()).Returns(mockPersonSet.Object);
mockContext.Setup(context => context.Persons).Returns(mockPersonSet.Object));

//Create the repo using the mock data context I created
PersonRepository repo = new PersonRepository(mockContext.Object);

//Then finally create the controller and perform the test
PersonController controller = new PersonController(repo);
var result = controller.Create(someEmployerID); //Sometimes throws an SQLException when DbContext.SaveChanges() is called
like image 605
Softerware Avatar asked Nov 26 '15 14:11

Softerware


1 Answers

I arrived at a less than ideal solution for myself but good enough to enable me to move on. I circumvented the DbContext.ChangeTracker.Entries() API by simply adding a level of abstraction to my class that extends DbContext called MyApplicationDbContext:

public class MyApplicationDbContext : IdentityDbContext<MyApplicationUser>
{
    //DbSets etc

    public virtual IEnumerable<MyModelBase> AddedEntries
    {
        get
        {               
            foreach (var entry in ChangeTracker.Entries().Where(entry => entry.State == EntityState.Added))
            {
                MyModelBase model = entry.Entity as MyModelBase;
                if (model != null)
                {
                    yield return model;
                }
            }
        }
    }
}

This way I can still iterate the Entries() for the business logic as described in the problem statement by calling MyApplicationDbContext.AddedEntries instead of MyApplicationDbContext.ChangeTracker.Entries(). However, since I made the property virtual, I can set up a return using Moq:

List<SomeModel> addedEntries = new List<SomeModel>();
addedEntries.add(someModelWhichWillBeAddedByTheController);
mockContext.Setup(context => context.AddedEntries).Returns(addedEntries);

This way the controller will observe someModelWhichWillBeAddedByTheController when the AddedEntries property is used. The down side is that I cannot test the DbContext.ChangeTracker.Entries() code used in the real business logic, but I will be able to achieve this later by implementing integration tests with a test database.

I was never able to find the reason for the SQLException being thrown on one machine and not the other.

like image 177
Softerware Avatar answered Oct 01 '22 12:10

Softerware