3

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.'

Joe
  • 177
  • 3
  • 16
  • `UserManager` is an implementation concern that should be abstracted out. would make the mocking and injection easier. – Nkosi Apr 18 '18 at 20:49
  • When testing Controller class you will not be able to test actual work of `AuthorizationAttribute`. Because `AuthorizationAttribute` consumed by middleware. For testing that you need to test full pipeline: create client(`HttpClient`), start your WebAPI, send request to the url you are testing and assert on response. – Fabio Apr 19 '18 at 02:43

2 Answers2

10

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>().

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • Would you recommend the integration test approach over unit testing when testing a controller with its methods? – Joe Apr 19 '18 at 16:48
  • Controller *actions*, yes. If you have some sort of utility method on your controller that's not an action you can unit test that. Actions are inherently integrations. You won't know if it's actually working unless you hit it via a request. – Chris Pratt Apr 19 '18 at 16:53
  • This answer really got me on the right track, but how do you "fake" the authenticated user such that you could run a test that shows the authorization passes? ie how do you test Authorize attribute applied? ie [Authorize(Policy = "Is18orOlder")] or whatever the policy may be? – Brad Oct 17 '18 at 19:53
  • Insert one or more test users into the in-memory database. Then, hit your login post action with the test client and sending the username and pass for the user you want to test. The response will contain your auth cookie. Use that to make your authenticated request via the test client again. Based on the user being tested, assert authorization passes or fails, accordingly. – Chris Pratt Oct 17 '18 at 19:57
  • I've done and seen some complicated stuff because I didn't know about this and neither did anyone I was working with. This is going to be a huge step forward in my integration testing. Thanks! – Scott Hannen Jul 07 '21 at 19:37
  • Beside this answers usefulness, thanks especially for the idea of branching based on environment within the original `Startup` and have test db context setup there instead of overriding settings within test host builders services. – dee zg Oct 28 '21 at 22:40
1

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