7

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
Softerware
  • 2,535
  • 3
  • 17
  • 21

3 Answers3

2

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.

Softerware
  • 2,535
  • 3
  • 17
  • 21
0

What your error is saying is that it can't create a connection to your database. This happens upon creation of your DbContext where EF will start doing things like check if your database needs to be migrated.

It seems to me that you need to mock the constructor of your dbcontext too. i'm not too familiar with moq itself but if i read your code correctly i don't see you mocking the constructor.

Batavia
  • 2,497
  • 14
  • 16
  • I don't see any value in mocking the constructor since my tests will not be connecting to any database, I need to be able to use the DbContext.ChangeTracker.Entries() API offline. The best I could do is provide an alternate connection string for it to fail to connect to. – Softerware Nov 30 '15 at 14:47
  • My point is that a DbContext expect a database connection. If you want a mocked DbContext then you should mock the constructor so when your DbContext is created you get a fake version - one without database connection – Batavia Nov 30 '15 at 15:22
0

The exception says that it cannot create a connection to DB, I suspect that you and your friend have different connection strings in app.config files, or your friend doesn't have access to the DB.

Actually you are writing an integration test, not unit test. Unit tests are written specifically for an object under tests. In your case code:

PersonController controller = new PersonController();
var result = controller.Create(someEmployerID);

should not use real implementation of Repository. You need to inject mocked instance of repository into PersonController. I assume that you are not using any IoC containtainers so to be able to inject it into your controller you can add another constructor:

private IRepository _repository;    
public PersonController() : this(new Repository()) //real implementation
{}

public PersonController(IRepository repository)
{
    _repository = repository;
}

// your test
var reporitory = new Mock<IRepository>();
var controller = new PersonController(repository.Object);
controller.CreateEmployee(someId);
// assert that your repository was called
repository.Verify(...);

This technique is called Poor Man's injection, it is not the recommended way of injection, but it is better than having concrete instances only.

Next if you want to write unit tests (not integration) for your Repository then you need to have an interface like IDbContext which will be a wrapper around your DbContext. And create 2 constructors for your Repository as in PersonController - parameter-less and with IDbContext.

UPDATE: Disregard my last statement regarding Repository and DbContext. I've checked the documentation DbChangeTracker.Entries() method is not virtual, meaning that you will not be able to mock it using Moq library. You need to use another mocking framework or test it using intergation tests (without mocked instances).

Community
  • 1
  • 1
Andrei Mihalciuc
  • 2,148
  • 16
  • 14
  • Hi, thanks for your input. Actually I left that part out of the sample code in error. I use a repository which allows me to pass my mocked DbContext into it. It's not a "real implementation" like you mentioned. All my HTTP GET methods on the controller work with the repository fine, the problem comes up when some logic in the repo tries to utilize the DbContext.ChangeTracker.Entries() API. – Softerware Nov 30 '15 at 14:46