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

DbContext.ChangeTracker in automated tests throws SQLException

我正在为我一直致力于熟悉 MVC、EntityFramework(代码优先)、单元测试和 Moq 的项目编写自动化测试。

我的 Repository class 中有一个部分,每当 Repository.SaveChanges() 被这样工作的控制器调用时,我的模型就会设置一个 LastModified 字段(MyModelBase是基数 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();
}

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

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

然而,在与我共享项目的朋友的机器上,当访问ChangeTracker.Entities() 时,他得到一个SQLException。我确实检查了 VS2015 中抛出的 SQLExceptions,但我这边没有输出或其他异常指示。

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)

最后,我的问题是:有没有一种方法可以使用 Moq 来模拟 ChangeTracker(我怀疑不是来自之前的调查),或者是否有另一种方法可以让我的 RepoSaveChanges() 自动设置一个属性?如果不访问 ChangeTracker.Entities(),我将需要更新逻辑来为每个具有它的模型类型设置 LastModified 字段。同样,我想避免使用框架的 API/part,因为顽固的测试并不理想。

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

更新: 已请求更多示例代码,所以让我进一步详细说明。我使用最小起订量模拟 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

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

在我看来,您也需要模拟 dbcontext 的构造函数。我不太熟悉最小起订量本身,但如果我正确阅读你的代码,我不会看到你在嘲笑构造函数。

异常说它无法创建到数据库的连接,我怀疑你和你的朋友在 app.config 文件中有不同的连接字符串,或者你的朋友没有访问数据库的权限。

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

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 编写 unit 测试(而不是 integration),那么您需要一个类似 IDbContext 的界面将成为您的 DbContext 的包装器。并为 Repository 创建 2 个构造函数,如 PersonController - 无参数且带有 IDbContext

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

我为自己找到了一个不太理想的解决方案,但足以让我继续前进。我通过简单地向我的 class 添加一个抽象级别来绕过 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 设置 return:

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

这样,当使用 AddedEntries 属性 时,控制器将观察到 someModelWhichWillBeAddedByTheController。不利的一面是我无法测试真正业务逻辑中使用的 DbContext.ChangeTracker.Entries() 代码,但稍后我将能够通过使用测试数据库实施集成测试来实现这一点。

我一直没能找到 SQLException 被扔在一台机器上而不是另一台机器上的原因。