自动化测试中的DbContext.ChangeTracker抛出SQLException

时间:2015-11-26 14:47:10

标签: c# asp.net-mvc entity-framework unit-testing moq

我正在为我正在努力的项目编写自动化测试,以便更熟悉MVC,EntityFramework(代码优先),单元测试和Moq。

我的Repository类中有一个部分,只要控制器调用LastModified,就会设置模型的Repository.SaveChanges()字段,其工作方式如下(MyModelBase是基类):

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();
}

这在正常在线环境中的应用程序运行时期间工作正常,但在测试方法中运行时会中断。我使用Moq来模拟DbContext中的DbSets并设置我的测试数据。

这是我的奇怪部分: 我的单元测试运行正常(通过)但它们实际上并没有进入foreach循环 - 当访问ChangeTracker.Entities()时它会挂起并退出循环,跳到_dbContext.SaveChanges()。没有错误。

但是,在与我共享项目的朋友的机器上,当访问ChangeTracker.Entities()时,他会收到SQLException。我确实在VS2015中检查了SQLExceptions,并且没有输出或其他异常指示。

  

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

     

结果讯息:
  测试方法MyProject.Tests.TestClasses.MyControllerTests.TestCreate抛出异常:   System.Data.SqlClient.SqlException:建立与SQL Server的连接时发生与网络相关或特定于实例的错误。服务器未找到或无法访问。验证实例名称是否正确,以及SQL Server是否配置为允许远程连接。 (提供程序:SQL网络接口,错误:26 - 查找指定的服务器/实例时出错)

最后,我的问题是:有没有办法使用Moq模拟ChangeTracker(我怀疑不是来自之前的调查),还是我可以采取另一种方法来自动设置属性的RepoSaveChanges()?在不访问ChangeTracker.Entities()的情况下,我需要有更新逻辑来为每个拥有它的模型类型设置LastModified字段。同样,我觉得避免使用该API /框架的一部分,因为测试很顽固并不理想。

有没有人知道为什么SQLException没有抛出/无法在我的机器上捕获?或者有关如何在单元测试中使用ChangeTracker.Entities()的任何建议?我只会在我的所有模型和控制器上单独设置LastModified属性作为最后的手段。

更新 已经请求了更多示例代码,所以让我进一步详细说明。我使用moq模拟DbContext,然后模拟DbContext中包含的DbSet对象:

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

3 个答案:

答案 0 :(得分:2)

我为自己找到了一个不太理想的解决方案,但足以让我继续前进。我通过简单地向我的类添加了一个抽象级别来绕过DbContext.ChangeTracker.Entries() API,该类扩展了DbContext 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;
                }
            }
        }
    }
}

这样我仍然可以通过调用MyApplicationDbContext.AddedEntries而不是MyApplicationDbContext.ChangeTracker.Entries()来迭代业务逻辑的Entries(),如问题陈述中所述。但是,由于我创建了属性virtual,我可以使用Moq设置返回:

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

这样,当使用someModelWhichWillBeAddedByTheController属性时,控制器将观察AddedEntries。缺点是我无法测试真实业务逻辑中使用的DbContext.ChangeTracker.Entries()代码,但我稍后可以通过使用测试数据库实现集成测试来实现此目的。

我永远无法找到SQLException被抛在一台机器而不是另一台机器上的原因。

答案 1 :(得分:0)

您的错误是说它无法创建与数据库的连接。在创建DbContext时会发生这种情况,其中EF将开始执行诸如检查是否需要迁移数据库之类的操作。

在我看来,您还需要模拟dbcontext的构造函数。我对moq本身并不太熟悉,但如果我正确地阅读了你的代码,我就不会看到你嘲笑构造函数。

答案 2 :(得分:0)

例外情况说它无法创建与DB的连接,我怀疑你和你的朋友在app.config文件中有不同的连接字符串,或者你的朋友无法访问数据库。

实际上您正在编写集成测试,而不是单元测试。单元测试专门针对测试对象编写。在您的案例代码中:

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

不应该使用Repository的真实实现。您需要将模拟的存储库实例注入PersonController。我假设你没有使用任何IoC containtainers,所以为了能够将它注入你的控制器,你可以添加另一个构造函数:

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(...);

这种技术称为Poor Man's injection,它不是推荐的注入方式,但它比仅具有具体实例更好。

接下来,如果您要为Repository编写单元测试(不是集成),那么您需要有一个类似IDbContext的界面将是DbContext的包装器。并为Repository创建2个构造函数,如PersonController - 参数少,使用IDbContext

更新:忽略我关于RepositoryDbContext的最后陈述。我已经检查了文档DbChangeTracker.Entries()方法不是虚拟的,这意味着你将无法使用Moq库来模拟它。您需要使用another mocking framework或使用 intergation 测试(没有模拟实例)对其进行测试。