C# EF操作SQL Server的NUnit测试报DbContext未配置提供程序错误
问题根因
单元测试运行时触发No database provider has been configured异常的核心原因是:测试项目运行在独立的执行上下文,没有加载主项目的连接字符串配置,PhonebookContext的OnConfiguring方法中读取ConnectionStrings["SQLServer"]时返回null,调用.ConnectionString抛出空引用异常,被内部catch块捕获后仅打印日志,未完成SQL Server提供程序的配置,最终EF Core在执行操作时抛出异常。
现存问题
- 测试项目缺失配置:主项目正常运行时会读取自身输出目录下配置文件中的连接字符串,但测试项目运行时不会自动复制、加载主项目的配置文件,
ConfigurationManager无法找到名为SQLServer的连接字符串项,导致配置流程中断。 - 断言写法错误:现有测试使用
Assert.Equals做判断,该方法是object基类的静态Equals方法,不会触发NUnit的断言逻辑,且Contact类默认做引用相等判断,即使属性完全一致,两个不同实例也会判定为不相等。 - 测试逻辑强依赖真实数据库:现有测试直接连接真实SQL Server,属于集成测试范畴,执行时会产生持久化脏数据,测试稳定性受数据库服务运行状态影响。
- 代码硬编码依赖:
Create方法内部直接实例化PhonebookContext,没有预留注入入口,测试时无法替换为测试专用的数据库上下文。
修复方案
快速修复(解决当前配置报错,可直接运行现有测试逻辑)
- 在测试项目中新增
App.config配置文件,添加和主项目一致的连接字符串节点,示例配置:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="SQLServer" connectionString="替换为测试环境可用的SQL Server连接字符串" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>
- 确认测试项目已安装和主项目版本完全一致的
Microsoft.EntityFrameworkCore.SqlServerNuGet包,避免版本不兼容导致提供程序加载失败。 - 修正测试断言的写法:
[Test] public void Create_Contact_DbRowIsAdded() { var contact = new Contact { Name = "Abhinav", PhoneNumber = 1234567890, }; SqlAccess.Create(contact); var lastContact = SqlAccess.GetLastContact(); Assert.That(lastContact.Name, Is.EqualTo(contact.Name)); Assert.That(lastContact.PhoneNumber, Is.EqualTo(contact.PhoneNumber)); }
注意:测试执行前需要确保配置中指向的SQL Server实例可正常访问,且测试执行完成后需要手动清理插入的测试数据,避免影响其他测试用例。
规范改造方案(推荐,实现不依赖外部服务的稳定单元测试)
- 改造
PhonebookContext,支持外部传入配置项,兼容生产环境和测试场景:
public class PhonebookContext : DbContext { // 保留无参构造函数供生产环境原有逻辑使用 public PhonebookContext() { } // 增加带配置参数的构造函数,供测试场景注入测试用配置 public PhonebookContext(DbContextOptions<PhonebookContext> options) : base(options) { } public DbSet<Contact> Contacts => Set<Contact>(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 仅当外部没有传入配置时,才读取本地配置文件的连接字符串 if (!optionsBuilder.IsConfigured) { var connStr = ConfigurationManager.ConnectionStrings["SQLServer"]?.ConnectionString; if (!string.IsNullOrEmpty(connStr)) { optionsBuilder.UseSqlServer(connStr); } } } }
- 改造
SqlAccess的静态方法,支持外部传入DbContext实例,避免硬编码依赖:
public static void Create(Contact contact, PhonebookContext context = null) { using (var db = context ?? new PhonebookContext()) { db.Database.EnsureCreated(); try { db.Contacts.Add(contact); db.SaveChanges(); Console.WriteLine($"Successfully added {contact.Name}!"); } catch { Console.WriteLine(Helpers.CreateErrorMessage, contact.Name); } } }
- 测试项目引入EF Core内存数据库提供程序,替换真实SQL Server作为测试数据源,不需要额外配置数据库服务:
- 首先在测试项目安装
Microsoft.EntityFrameworkCore.InMemoryNuGet包,版本和项目使用的EF Core版本保持一致。 - 改造测试用例:
[Test] public void Create_Contact_DbRowIsAdded() { // 构建内存数据库配置,每个测试用例使用独立的数据库名,避免用例间数据干扰 var options = new DbContextOptionsBuilder<PhonebookContext>() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; var contact = new Contact { Name = "Abhinav", PhoneNumber = 1234567890 }; // 插入数据使用独立上下文实例 using (var createContext = new PhonebookContext(options)) { SqlAccess.Create(contact, createContext); } // 验证数据使用新的上下文实例,模拟真实场景下的数据库读取逻辑 using (var verifyContext = new PhonebookContext(options)) { var lastContact = verifyContext.Contacts.OrderByDescending(c => c.Id).First(); Assert.That(lastContact.Name, Is.EqualTo(contact.Name)); Assert.That(lastContact.PhoneNumber, Is.EqualTo(contact.PhoneNumber)); } }
内容的提问来源于stack exchange,提问作者Abhinav
相关产品推荐
相关产品推荐

