使用Ninject MockingKernel模拟ILog接口时遇到的异常问题
如何用Ninject.MockingKernel Mock BaseController中直接创建的ILog实例?
我正在给遗留ASP.NET代码添加单元测试,使用Moq和Ninject.MockingKernel。现有代码结构如下:
BaseController类:
public class BaseController : Controller { private IAuthenticationManager authenticationManager; public ILog log; public BaseController() { var kernel = new StandardKernel(); kernel.Load(Assembly.GetExecutingAssembly()); log = kernel.Get<ILog>(); } }
ILog接口:
public interface ILog { AuditTrail Create(AuditAction action, string description = null); AuditTrail Create(AuditAction action, long reservationId, string description = null); AuditTrail Create(IUser user, AuditAction action); AuditTrail Create(IUser user, AuditAction action, string description = null); AuditTrail Create(IUser user, AuditAction action, long reservationId, string description = null); }
我的需求是Mock BaseController中通过StandardKernel获取的ILog实例(该实例被其他控制器继承且未采用注入方式传入),希望测试时让BaseController拿到Mock的ILog对象,类似Mock DatabaseContext的场景。
参考文档后我尝试了以下代码:
mockingKernal = new MoqMockingKernel(); mockingKernal.Bind<ILog>().To<Logging.Log>(); var foo = mockingKernal.GetMock<ILog>(); foo.Setup(x => x.Create(It.IsAny<AuditAction>(), It.IsAny<long>(), It.IsAny<string>()));
但运行时抛出异常:System.ArgumentException: Object instance was not created by Moq。查阅资料得知该异常通常由构造函数含参数导致,但我的Log类是无参构造。请问我的处理方式是否正确?若正确,问题出在哪里?
问题分析
你的核心问题有两个:
- 容器实例不共享:BaseController的构造函数里自己创建了一个全新的
StandardKernel,和测试代码中的MoqMockingKernel是完全独立的两个容器。不管你在测试的MockingKernel里怎么配置,BaseController都会从它自己创建的StandardKernel中获取真实的Log实例,根本不会用到你Mock的对象。 - Mock绑定冲突:你调用
mockingKernal.Bind<ILog>().To<Logging.Log>()把ILog绑定到了真实的Log类,之后再调用GetMock<ILog>()就会触发异常——因为MoqMockingKernel默认会为接口自动生成Mock实例,但你已经手动绑定到了具体实现,此时GetMock无法从真实实例生成Mock对象,所以抛出"Object instance was not created by Moq"的错误。
解决方案
方案1:改造BaseController(推荐,长期更合理)
虽然是遗留代码,但让BaseController支持依赖注入会让测试变得简单且可维护。可以修改构造函数,保留原有无参构造以兼容现有代码,同时新增带参数的构造函数供测试使用:
public class BaseController : Controller { private IAuthenticationManager authenticationManager; public ILog log; // 保留无参构造,兼容现有业务代码 public BaseController() : this(new StandardKernel()) { } // 供测试使用的构造函数,接受自定义Kernel public BaseController(IKernel kernel) { kernel.Load(Assembly.GetExecutingAssembly()); log = kernel.Get<ILog>(); } // 更优的方式:直接注入ILog(符合依赖注入原则) // public BaseController(ILog logInstance) { // log = logInstance; // } }
测试代码修改为:
// 初始化MockingKernel并创建ILog的Mock var mockingKernel = new MoqMockingKernel(); var logMock = mockingKernel.GetMock<ILog>(); logMock.Setup(x => x.Create(It.IsAny<AuditAction>(), It.IsAny<long>(), It.IsAny<string>())); // 使用带参数的构造函数创建BaseController实例 var controller = new BaseController(mockingKernel); // 此时controller.log就是你配置的Mock实例
方案2:不修改BaseController的临时方案
如果暂时不能改动BaseController的构造函数,可以通过反射替换它的log字段来注入Mock实例:
// 创建ILog的Mock并配置 var logMock = new Mock<ILog>(); logMock.Setup(x => x.Create(It.IsAny<AuditAction>(), It.IsAny<long>(), It.IsAny<string>())); // 创建BaseController实例 var controller = new BaseController(); // 用反射替换public的log字段 typeof(BaseController) .GetField("log", BindingFlags.Public | BindingFlags.Instance) .SetValue(controller, logMock.Object);
这个方案属于“ hack 式”的临时解决办法,破坏了类的封装性,一旦BaseController后续修改了log字段的访问修饰符或名称,测试就会失效,所以仅推荐作为短期过渡方案。
内容的提问来源于stack exchange,提问作者jmo
相关产品推荐
相关产品推荐

