【问题标题】:DbContext.ChangeTracker in automated tests throws SQLException自动化测试中的 DbContext.ChangeTracker 抛出 SQLException
【发布时间】:2016-02-29 17:09:37
【问题描述】:

我正在为我一直从事的项目编写自动化测试,以便更加熟悉 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 中模拟 DbSet 并设置我的测试数据。

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

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

结果堆栈跟踪:
在 System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject,UInt32 waitForMultipleObjectsTimeout,布尔allowCreate,布尔onlyOneCheckConnection,DbConnectionOptions userOptions,DbConnectionInternal& 连接) ....

结果消息:
测试方法 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

【问题讨论】:

  • 这是什么版本的EF?
  • @E-Bat EF 版本 6.1.3

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


【解决方案1】:

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

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

【讨论】:

  • 我看不到任何模拟构造函数的价值,因为我的测试不会连接到任何数据库,我需要能够离线使用 DbContext.ChangeTracker.Entries() API。我能做的最好的就是提供一个备用连接字符串,让它无法连接。
  • 我的意思是 DbContext 需要一个数据库连接。如果你想要一个模拟的 DbContext,那么你应该模拟构造函数,这样当你的 DbContext 被创建时,你会得到一个假的版本 - 一个没有数据库连接的版本
【解决方案2】:

异常说它无法创建到数据库的连接,我怀疑你和你的朋友在 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 编写单元 测试(而不是集成),那么您需要有一个类似IDbContext 的接口,它将是一个包装器在您的DbContext 周围。并为您的Repository 创建 2 个构造函数,如PersonController - 无参数和IDbContext

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

【讨论】:

  • 您好,感谢您的意见。实际上,我错误地将那部分从示例代码中删除。我使用了一个存储库,它允许我将模拟的 DbContext 传递给它。这不是您提到的“真正的实现”。我在控制器上的所有 HTTP GET 方法都可以正常使用存储库,当存储库中的某些逻辑尝试利用 DbContext.ChangeTracker.Entries() API 时,就会出现问题。
【解决方案3】:

我为自己找到了一个不太理想的解决方案,但足以让我继续前进。我绕过了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);

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-01
    • 1970-01-01
    相关资源
    最近更新 更多