如何使用FakeItEasy模拟ASP.NET Core控制器基类的受保护方法?
问题
我正尝试为ASP.NET Core应用的REST API控制器添加基于FakeItEasy的单元测试。需要测试的公共控制器方法会调用父类中实现的受保护授权方法Authorize,该方法依赖单元测试中无法获取的运行时数据。请问在不做大的应用设计改动的前提下,绕过单元测试中的授权调用、让基类的受保护方法始终执行成功的最佳方式是什么?
一种方案是将授权调用实现为独立接口,但这需要修改应用设计,目前我希望在不做重大改动的前提下让单元测试正常运行。
相关代码概要
基控制器类
[ApiController] public abstract class MicroserviceController: Microsoft.AspNetCore.Mvc.ControllerBase { protected readonly ICorrelationContextAccessor _correlation; protected readonly IConfiguration _config; protected readonly ILogger _logger; protected MicroserviceController ( IConfiguration config, ILogger logger, ICorrelationContextAccessor correlation ) { _config = config; _logger = logger; _correlation = correlation; } protected virtual void Authorize(string[] scopes, string[] roles) { // 依赖ControllerBase方法和属性的授权逻辑 } // 其他方法 }
控制器类
[ApiController] public class UsersController: MicroserviceController { private IUserService _userService; public UsersController ( IConfiguration config, ILogger<UsersController> logger, ICorrelationContextAccessor correlation, IUserService userService ) : base(config, logger, correlation) { _userService = userService; } [HttpGet] public ActionResult<User> GetUser(string userId) { try { Authorize(new string[] { "user_read", "user_write", "user_delete" }, null); } catch (UnauthorizedException ex) { return Unauthorized(ProcessError(ex)); } catch (Exception ex) { return BadRequest(ProcessError(ex)); } User user; try { user = _userService.GetUserById(userId); } catch (UnauthorizedException ex) { return Unauthorized(ProcessError(ex)); } catch (NotFoundException ex) { return NotFound(ProcessError(ex)); } catch (Exception ex) { return BadRequest(ProcessError(ex)); } return Ok(user); } }
单元测试类
public class UsersControllerTests { private IConfiguration _config; private ILogger<UsersController> _logger; private ICorrelationContextAccessor _correlation; private IUserService _userService; public UsersControllerTests() { _config = A.Fake<IConfiguration>(); _logger = A.Fake<ILogger<UsersController>>(); _correlation = A.Fake<ICorrelationContextAccessor>(); _userService = A.Fake<IUserService>(); } [Fact] public void UsersController_GetUser_ReturnOk() { // Arrange UsersController usersController = new UsersController(_config, _logger, _correlation, _userService); string userId = "123456789"; User user = A.Fake<User>(); A.CallTo(() => _userService.GetUserById(userId)).Returns(user); // 如何让usersController.Authorize(scopes, roles)调用不执行任何逻辑? // Act ActionResult<User> result = usersController.GetUser(userId); // Assert result.Should().NotBeNull(); } }
请问是否可以抑制UsersController的GetUser方法对Authorize的调用?还是说我应该开始重新设计应用?
解决方案
不需要重新设计应用,利用FakeItEasy可以直接拦截基类中标记为virtual的受保护方法,让它在测试中不执行原有逻辑也不抛出异常。具体实现如下:
修改后的测试代码
[Fact] public void UsersController_GetUser_ReturnOk() { // Arrange // 创建UsersController的Fake实例,传入构造参数 var usersController = A.Fake<UsersController>(_config, _logger, _correlation, _userService); // 拦截所有对Authorize方法的调用,使其不执行任何逻辑 A.CallTo(usersController) .Where(call => call.Method.Name == "Authorize") .DoesNothing(); string userId = "123456789"; User user = A.Fake<User>(); A.CallTo(() => _userService.GetUserById(userId)).Returns(user); // 指定GetUser方法执行真实的控制器逻辑,而非Fake默认的空实现 A.CallTo(() => usersController.GetUser(userId)).CallsBaseMethod(); // Act ActionResult<User> result = usersController.GetUser(userId); // Assert result.Should().NotBeNull(); result.Result.Should().BeOfType<OkObjectResult>(); ((OkObjectResult)result.Result).Value.Should().Be(user); }
核心逻辑说明
- 因为
Authorize是基类中标记为virtual的受保护方法,FakeItEasy能够识别并拦截它的调用。如果方法未标记为virtual,这种方式会失效,但你的代码刚好满足条件。 - 使用
CallsBaseMethod()确保GetUser方法执行真实的业务逻辑,避免被FakeItEasy替换为空实现。 - 整个方案完全在测试层修改,不需要改动任何应用代码,符合你“不做大设计改动”的需求。
后续优化建议(可选)
如果之后希望代码结构更解耦,可以将授权逻辑抽象为独立的IAuthorizationService接口,注入到控制器中。测试时直接Fake这个接口即可,无需依赖虚方法拦截。但这属于代码重构,目前可以先用上述方案快速解决测试问题。
内容的提问来源于stack exchange,提问作者Alek Davis
相关产品推荐
相关产品推荐

