自动化测试中的DbContext.ChangeTracker引发SQLException

本文关键字:引发 SQLException ChangeTracker DbContext 自动化测试 | 更新日期: 2023-09-27 18:24:29

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

我的Repository类中有一个部分,每当控制器调用Repository.SaveChanges()时,它都会设置我的模型的LastModified字段,如下所示(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中检查了抛出的SQLException,并且没有任何异常的输出或其他指示。

结果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

自动化测试中的DbContext.ChangeTracker引发SQLException

我找到了一个不太理想的解决方案,但足以让我继续前进。我通过简单地向我的类添加一个抽象级别来扩展DbContext,称为MyApplicationDbContext:,从而避开了DbContext.ChangeTracker.Entries() API

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被扔到一台机器上而不是另一台机器的原因。

您的错误是它无法创建与数据库的连接。这发生在创建DbContext时,EF将开始做一些事情,比如检查数据库是否需要迁移。

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

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

实际上,您正在编写集成测试,而不是单元。单元测试是专门为被测试对象编写的。在您的情况下,代码:

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

不应使用CCD_ 23的实际实现。您需要将存储库的模拟实例注入到PersonController中。我假设您没有使用任何IoC容器,因此为了能够将其注入控制器,您可以添加另一个构造函数:

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

这种技术被称为穷人注射,这不是推荐的注射方式,但它比只有具体的例子要好。

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

更新:忽略我关于RepositoryDbContext的最后一句话。我已经检查了文档DbChangeTracker.Entries()方法不是虚拟的,这意味着您将无法使用Moq库来模拟它。您需要使用另一个mocking框架,或者使用intergate测试来测试它(没有mocking实例)。