I'm trying to add FakeItEasy-based unit tests to a REST API controller of an ASP.NET core app. The public controller methods I need to test call the protected authorization methods implemented in the parent class that rely on the runtime data not available in unit tests. What is the best way to bypass the explicit authorization calls from unit tests? In the other words, how do I make the protected base class method to always succeed?
One option would be to implement authorization calls as a separate interface, but this would require changing the application design, and I would like to make unit tests work without making major changes for now.
Here is the outline of relevant code.
Base controller class:
[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;
}
virtual protected Authorize(string[] scopes, string[] roles)
{
// Authorization logic that relies on the ControllerBase methods and properties.
}
// More methods.
}
Controller class:
[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);
}
Unit test class:
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);
// HOW DO I FORCE the usersController.Authorize(scopes, roles) CALL TO DO NOTHING?
// Act
ActionResult<User> result = usersController.GetUser(userId);
// Assert
result.Should().NotBeNull();
}
}
Is it possible to suppress the Authorize
calls made by the GetUser
call from the usersController
object? Or should I start working on the application redesign?
HOW DO I FORCE the _userService.Authorize(scopes, roles) CALL TO DO NOTHING?
I assume you mean the usersController.Authorize(scopes, roles)
method, since the code contains no mention of _userService.Authorize
?
For that, you would need the usersController
to be a fake, and then you'd be able to configure the Authorize
method as shown here:
A.CallTo(usersControllers)
.Where(call => call.Method.Name == "Authorize")
.DoesNothing();
However, that's not a very good approach... Normally you should never have to fake the SUT (subject under test): you're supposed to fake dependencies, not the class you want to test. But with your current design, I see no other option. You can't use FakeItEasy to configure a method of an object that isn't a fake.
Now, if the MicroserviceController.Authorize
called another service (e.g. IUserService
), you could fake that service and configure what it does. This is the approach I would recommend.