Search code examples
c#unit-testingdependency-injectionconstructor

How to deal with a Controller containing two constructors in ASP.NET Core?


I have an API upon which I mean to do Unit Testing via xUnit and Moq. It had one constructor actually, but I had to add another one for the specific purpose of Testing as per a suggestion by Visual Studio. It worked perfect for Testing, but I encountered another error once I tried to test the API via Swagger. "System.InvalidOperationException: Multiple constructors accepting all given argument types have been found in type 'Product.API.Controllers.ProductController'. There should only be one applicable constructor". below is the contents of the controller

public class ProductController : ControllerBase
{
    private readonly IMediator _mediator;
    private readonly IConfiguration _configuration;
    private readonly UserManager<User> _userManager;
    private readonly SignInManager<User> _signInManager;
    private readonly ProductDbContext _productDbContext;
    private readonly IUserService _userService;

    public ProductController(IConfiguration configuration, UserManager<User> userManager, ProductDbContext productDbContext
        , SignInManager<User> signInManager, IUserService userService)
    {
        _configuration = configuration;
        _userManager = userManager;
        _productDbContext = productDbContext;
        _signInManager = signInManager;
        _userService = userService;
    }
    public ProductController(IMediator mediator)
    {
        _mediator = mediator;
    }
    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> GetAllProducts()
    {
        var products = await _mediator.Send(new GetAllProductsQuery());
        return Ok(products);
    }

Below is the code for Unit Testing

private readonly Mock<IMediator> _mediatorMock;
private readonly ProductController _controller;
public ProductControllerTest()
{
    _mediatorMock = new Mock<IMediator>();
    _controller = new ProductController(_mediatorMock.Object);
}
[Fact]
public async Task ProductController_GetAllProducts_ShouldReturnStatusCode200()
{
    //arrange
    var expected = new List<ProductModelDTO>();
    _mediatorMock.Setup(m=> m.Send(It.IsAny<GetAllProductsQuery>(), default(CancellationToken))).ReturnsAsync(expected);
    //act
    var result = await _controller.GetAllProducts();
    //assert
    var okResult = result as OkObjectResult;
    okResult?.StatusCode.Should().Be(200);
    okResult?.Value.Should().BeOfType<List<ProductModelDTO>>();
}

I came across a possible solution; to add a [FromServices] to the second constructor. But I got the same error!


Solution

  • Move all dependencies to a single ctor:

    public ProductController(IConfiguration configuration, 
        UserManager<User> userManager, 
        ProductDbContext productDbContext, 
        SignInManager<User> signInManager, 
        IUserService userService, 
        IMediator mediator)
    {
        // ...
    }
    

    And then pass all values to it in test:

    public ProductControllerTest()
    {
        _mediatorMock = new Mock<IMediator>();
        // mock and pass everything:
        _controller = new ProductController(..., _mediatorMock.Object); 
    }
    

    Alternatively you can use the ASP.NET Core ability to use method injection (Action injection with FromServices) though you still will need to pass all common parameters to ProductController ctor, but you can reduce the overall number of them and manage non-shared ones only where they are needed:

    public ProductController(IConfiguration configuration, UserManager<User> userManager, ProductDbContext productDbContext
        , SignInManager<User> signInManager, IUserService userService)
    {
        _configuration = configuration;
        _userManager = userManager;
        _productDbContext = productDbContext;
        _signInManager = signInManager;
        _userService = userService;
    }
    
    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> GetAllProducts([FromServices] IMediator mediator)
    {
        var products = await _mediator.Send(new GetAllProductsQuery());
        return Ok(products);
    }
    

    Note that in general injecting both IMediator and things like db contexts and separate services, and looks like a smell, common approach is either use IMediator for (almost) everything if using it at all.