使用Moq模拟Oracle DbContext调用Entry时出现连接字符串缺失错误
你遇到的这个问题核心在于Moq无法模拟DbContext的非虚方法,而Entry<T>方法在DbContext中默认是非虚的。让我一步步拆解原因和解决办法:
为什么会触发连接字符串错误?
当你调用_context.Entry<ACTIVITY_CODE>(found)时,虽然你用Moq创建了Entities的模拟对象,但Moq只能拦截虚方法或抽象方法的调用。DbContext.Entry<T>是一个非虚的实例方法,所以这个调用会直接落到真实的DbContext实现上——而真实的Entities构造函数需要读取配置文件里的"Entities"连接字符串,你的测试环境里没有这个配置,自然就抛出了异常。
简单说:你的模拟上下文只覆盖了ACTIVITY_CODE这个DbSet,但Entry方法没被模拟,执行了真实逻辑,导致触发了连接字符串的检查。
解决办法
办法1:重写Entry方法,让它可被Moq模拟
在你的partial Entities类中添加一个虚的Entry方法,这样Moq就能拦截这个调用:
public partial class Entities : System.Data.Entity.DbContext { public Entities(string scon) : base(scon) { } // 添加虚的Entry方法,覆盖基类的非虚方法 public virtual DbEntityEntry<T> Entry<T>(T entity) where T : class { return base.Entry(entity); } }
然后在测试中模拟Entry方法,返回一个DbEntityEntry的模拟对象,让CurrentValues.SetValues不会执行真实逻辑:
[TestMethod] public void activity_code_update_test() { // 准备mockSet的代码保持不变... var mockContext = new Mock<Entities>(); mockContext.Setup(c => c.ACTIVITY_CODE).Returns(mockSet.Object); // 模拟Entry方法,返回一个Mock的DbEntityEntry var mockEntry = new Mock<DbEntityEntry<ACTIVITY_CODE>>(); mockContext.Setup(c => c.Entry(It.IsAny<ACTIVITY_CODE>())).Returns(mockEntry.Object); // 模拟CurrentValues.SetValues的行为(可选,如果你需要验证这个调用) mockEntry.Setup(e => e.CurrentValues.SetValues(It.IsAny<ACTIVITY_CODE>())).Verifiable(); // 后续测试代码保持不变... var expected = new ACTIVITY_CODE() { ACT_ID = 1, ACT_CODE = "code 2", ACT_DESC = "desc 2" }; var target = new ActivityCodeService(mockContext.Object); // 执行 target.Update(expected); // 验证SetValues是否被调用(可选) mockEntry.Verify(); }
办法2:保持直接修改实体属性的写法(推荐用于单元测试)
你已经发现,直接修改找到的实体属性(ret.ACT_CODE = item.ACT_CODE;)的方式测试正常,这种写法其实更适合单元测试场景——因为它不需要依赖Entry方法的内部逻辑,只需要验证实体被修改后SaveChanges是否被调用即可。
如果你想验证SaveChanges的调用,可以在测试中添加:
// 在准备mockContext后添加 mockContext.Setup(c => c.SaveChanges()).Returns(1); // 执行测试后验证 mockContext.Verify(c => c.SaveChanges(), Times.Once);
办法3:改用接口抽象DbContext(更优雅的长期方案)
如果你的项目有长期的测试需求,可以抽象一个IEntities接口,让Entities实现这个接口,然后在服务中依赖接口而非具体的DbContext。这样Moq模拟接口会更简单,也避免了依赖EF的具体实现:
public interface IEntities { DbSet<ACTIVITY_CODE> ACTIVITY_CODE { get; set; } DbEntityEntry<T> Entry<T>(T entity) where T : class; int SaveChanges(); } public partial class Entities : DbContext, IEntities { // 保持原有代码不变,自动实现IEntities接口 } // 修改服务类依赖接口 public partial class ActivityCodeService { private IEntities _context; public ActivityCodeService(IEntities context) { this._context = context; } // Update方法保持不变 } // 测试时模拟接口 [TestMethod] public void activity_code_update_test() { var mockSet = new Mock<DbSet<ACTIVITY_CODE>>(); // 准备mockSet的代码不变... var mockContext = new Mock<IEntities>(); mockContext.Setup(c => c.ACTIVITY_CODE).Returns(mockSet.Object); var mockEntry = new Mock<DbEntityEntry<ACTIVITY_CODE>>(); mockContext.Setup(c => c.Entry(It.IsAny<ACTIVITY_CODE>())).Returns(mockEntry.Object); // 后续测试代码不变... }
总结
- 核心原因:
DbContext.Entry是非虚方法,Moq无法拦截,导致执行真实逻辑触发连接字符串检查。 - 最快解决:要么重写
Entry方法让它可被模拟,要么继续用直接修改属性的写法(推荐单元测试用这个)。 - 长期方案:抽象DbContext为接口,降低耦合度,让测试更灵活。
内容的提问来源于stack exchange,提问作者Ray Cheng

