Joe
Joe

Reputation: 187

Unit Testing Controller with Authorization ASP.NET Core 2.0 Web API

I have a controller:

public class InvitationsController: Controller {
        private readonly IMapper _mapper;
        private readonly IInvitationManager _invitationManager;
        private readonly UserManager<MyAppUser> _userManager;

        public InvitationsController(
            IInvitationManager invitationManager,
            IMapper mapper,
            UserManager<MyAppUser> userManager,
            IJobManager jobManager
        ) {
            _invitationManager = invitationManager;
            _mapper = mapper;
            _userManager = userManager;
        } 
[Authorization]
GetInvitationByCode(string code) { ... }

I'm trying to write unit tests using Xunit and Moq. Here is the implentation of my test:

  public class InvitationsControllerTests {

    private Mock<IInvitationManager> invitationManagerMock;        
    private Mock<UserManager<MyAppUser>> userManagerMock;
    private Mock<IMapper> mapperMock;
    private InvitationsController controller;

    public InvitationsControllerTests() {
        invitationManagerMock = new Mock<IInvitationManager>();          
        userManagerMock = new Mock<UserManager<MyAppUser>>();

        mapperMock = new Mock<IMapper>();
        controller = new InvitationsController(invitationManagerMock.Object,
                   mapperMock.Object,
                   userManagerMock.Object);
    }

    [Fact]
    public async Task GetInvitationByCode_ReturnsInvitation() {

        var mockInvitation = new Invitation {
            StoreId = 1,
            InviteCode = "123abc",
        };

        invitationManagerMock.Setup(repo => 
        repo.GetInvitationByCodeAsync("123abc"))
            .Returns(Task.FromResult(mockInvitation));

        var result = await controller.GetInvitationByCode("123abc");

        Assert.Equal(mockInvitation, result);
    }

I don't think I'm using the mocking functionality correctly. Specifically with UserManager. I can't find a clear answer on using Moq to test controllers protected by [Authorize]. When running my tests, it throws an exception on

        controller = new InvitationsController(invitationManagerMock.Object,
                   mapperMock.Object,
                   userManagerMock.Object);

Which reads:

Castle.DynamicProxy.InvalidProxyConstructorArgumentsException: 'Can not instantiate proxy of class: Microsoft.AspNetCore.Identity.UserManager`1[[MyApp.api.Core.Models.MyAppUser, MyApp.api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]. Could not find a parameterless constructor.'

Upvotes: 3

Views: 7279

Answers (2)

Chris Pratt
Chris Pratt

Reputation: 239440

You're not unit testing; you're integration testing. When you find yourself setting up ten thousand mocks just to run a method, that's a pretty good sign it's an integration test. Additionally, things like authorization only happen as part of the request lifecycle; there's no way to test that, without doing an actual request, which again, means you're integration testing.

As such, use the test host.

private readonly TestServer _server;
private readonly HttpClient _client;

public MyTestClass()
{
    _server = new TestServer(new WebHostBuilder()
        .UseStartup<Startup>());
    _client = _server.CreateClient();
}

[Fact]
public async Task GetInvitationByCode_ReturnsInvitation() {

    var mockInvitation = new Invitation {
        StoreId = 1,
        InviteCode = "123abc",
    };

    var response = await _client.GetAsync("/route");
    response.EnsureSuccessStatusCode();

    var responseString = await response.Content.ReadAsStringAsync();
    var result = JsonConvert.DeserializeObject<Invitation>(responseString);

    // Compare individual properties you care about.
    // Comparing the full objects will fail because of reference inequality
    Assert.Equal(mockInvitation.StoreId, result.StoreId);
}

If you need to scaffold your data to make the correct result return, simply use the in-memory database provider. The easiest way to use this for integration testing is to specify a new environment like "Test". Then, in your startup, when configuring your context, branch on the environment and use the in-memory provider (instead of SQL Server or whatever) when the environment is "Test". Then, when setting up your test server for integration testing, simply add .UseEnvironment("Test") before .UseStartup<Startup>().

Upvotes: 10

Artem Kurianov
Artem Kurianov

Reputation: 64

I think, problem is in dependency injection. In your Startups.cs file you could find similar string: services.AddIdentity<AppUser, AppRole>().AddEntityFrameworkStores<AppDbContext>().AddDefaultTokenProviders(); it means that magic of namespace Microsoft.Extensions.DependencyInjection provide you an instance of your User- or RoleManger anywhere where you want to use it. For example, in InvitationsController using injectin in constructor.

You can try inject UserManger in test class and mock it. Or read similar question

Upvotes: 1

Related Questions