使用NUnit测试ASP.NET MVC控制器时,如何避免调用基类构造函数?
解决方法与替代方案
一、重构BaseController,用依赖注入替代构造函数里的硬逻辑
这是最靠谱的长期方案——基类构造里直接搞读配置、连数据库这种有副作用的操作,本身就违背了可测试性原则。
- 先把这些依赖抽象成接口:比如
IConfigReader(读配置)、IDbConnectionFactory(创建数据库连接)、ILogger(写日志) - 修改BaseController,通过构造函数注入这些接口,把原来构造里的逻辑换成依赖调用:
public class BaseController : Controller { protected readonly IConfigReader _configReader; protected readonly IDbConnectionFactory _dbConnectionFactory; protected readonly ILogger _logger; // 用带参构造替代无参构造,把依赖传进来 public BaseController(IConfigReader configReader, IDbConnectionFactory dbConnectionFactory, ILogger logger) { _configReader = configReader; _dbConnectionFactory = dbConnectionFactory; _logger = logger; // 原来构造里的操作,要么通过依赖执行,要么移到具体方法里延迟执行 } }
- 然后SomeController也要把这些依赖传递给基类:
public class SomeController : BaseController { public SomeController(IConfigReader configReader, IDbConnectionFactory dbConnectionFactory, ILogger logger) : base(configReader, dbConnectionFactory, logger) { } // 你要测试的方法 }
- 测试的时候,用Moq这类框架模拟这些接口,注入到SomeController里,这样基类构造就不会触发真实的配置读取、数据库连接了:
[Test] public void SomeController_SomeMethod_Test() { // 模拟各个依赖 var mockConfig = new Mock<IConfigReader>(); var mockDbFactory = new Mock<IDbConnectionFactory>(); var mockLogger = new Mock<ILogger>(); // 创建控制器实例 var controller = new SomeController(mockConfig.Object, mockDbFactory.Object, mockLogger.Object); // 执行测试逻辑 }
二、用反射绕过基类构造(临时应急,不推荐)
如果暂时没法改基类,只能用这种hack方式——通过反射创建控制器实例,跳过所有构造函数的执行:
[Test] public void SomeController_SomeMethod_Test_WithReflection() { // 用FormatterServices创建未初始化的实例,不调用任何构造 var controller = FormatterServices.GetUninitializedObject(typeof(SomeController)) as SomeController; // 如果测试的方法依赖基类里的字段,得手动用反射给这些字段赋值 var configField = typeof(BaseController).GetField("_configReader", BindingFlags.NonPublic | BindingFlags.Instance); var mockConfig = new Mock<IConfigReader>(); configField.SetValue(controller, mockConfig.Object); // 继续执行测试 }
注意:这种方式会跳过所有构造函数(包括SomeController自己的),所以所有必要的字段、属性都得手动初始化,不然很容易出空引用异常,而且代码维护性极差,能不用就不用。
三、写测试专用的控制器子类
如果基类完全动不了,可以搞个测试用的假基类,再写个对应测试控制器:
- 先写
TestBaseController,继承Controller,空实现或者模拟基类的必要逻辑:
public class TestBaseController : Controller { // 覆盖基类的初始化方法,不执行原逻辑 protected override void Initialize(RequestContext requestContext) { // 啥也不做,或者按需模拟 } }
- 然后写
SomeControllerTest,继承这个测试基类,把原SomeController里要测试的方法复制过来(如果原类是部分类,也可以用部分类拆分):
public class SomeControllerTest : TestBaseController { // 和原SomeController里的方法完全一致 public ActionResult SomeMethod() { // 原方法逻辑 } }
- 测试的时候直接测这个
SomeControllerTest就行:
[Test] public void SomeMethod_Test() { var controller = new SomeControllerTest(); // 执行测试 }
这种方式的问题是,原控制器方法改了,测试类也得同步改,很麻烦,只适合完全没法重构基类的极端场景。
总结
优先选重构基类用依赖注入,这是从根源上解决问题的方案,还能让代码更符合设计原则。反射和测试专用子类都是临时妥协,能不用就不用。
内容的提问来源于stack exchange,提问作者Man Mohan Mundhra
相关产品推荐
相关产品推荐

