使用ASP.NET Core 2.0 Web API进行授权的单元测试控制器

3

我有一个控制器:

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) { ... }

我正在尝试使用Xunit和Moq编写单元测试。以下是我的测试实现:

  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);
    }

我觉得我的Mocking功能使用不正确。特别是在UserManager上。我无法找到一个清晰的答案来使用Moq测试由[Authorize]保护的控制器。在运行我的测试时,它会抛出一个异常。

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

错误信息如下:

Castle.DynamicProxy.InvalidProxyConstructorArgumentsException: '无法实例化代理类:Microsoft.AspNetCore.Identity.UserManager`1[[MyApp.api.Core.Models.MyAppUser, MyApp.api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]。 找不到无参构造函数。'


UserManager 是一个应该被抽象出来的实现关注点。这样会使得模拟和注入更加容易。 - Nkosi
在测试控制器类时,您将无法测试AuthorizationAttribute的实际工作。因为AuthorizationAttribute被中间件所使用。要测试它,您需要测试整个管道:创建客户端(HttpClient),启动您的WebAPI,向您正在测试的URL发送请求并对响应进行断言。 - Fabio
2个回答

10
你并没有进行单元测试,而是在进行集成测试。当你发现需要设置上万个模拟对象才能运行一个方法时,这就是一个明显的集成测试信号。此外,像授权这样的事情只会在请求生命周期中发生;如果不进行实际请求,就无法测试,这意味着你正在进行集成测试。
因此,请使用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);
}

如果您需要搭建数据以返回正确的结果,只需使用内存数据库提供程序即可。在集成测试中使用它的最简单方法是指定一个新的环境,比如"Test"。然后,在启动时,当配置上下文时,根据环境进行分支,并在环境为"Test"时使用内存提供程序(而不是SQL Server或其他)。然后,在设置集成测试的测试服务器时,只需在.UseStartup<Startup>()之前添加.UseEnvironment("Test")即可。

当测试控制器及其方法时,您会推荐集成测试方法胜过单元测试吗? - Joe
控制器的 actions,没错。如果您的控制器上有某种不是 action 的实用方法,您可以对其进行单元测试。Actions 本质上是集成。除非您通过请求访问它,否则您不会知道它是否真正起作用。 - Chris Pratt
这个答案确实让我找到了正确的方向,但是你如何“伪造”已认证的用户,以便运行一个测试来展示授权通过呢?也就是说,你如何测试应用了Authorize属性?比如[Authorize(Policy = "Is18orOlder")]或其他策略? - Brad
将一个或多个测试用户插入内存数据库。然后,使用测试客户端发送要测试的用户的用户名和密码,触发登录请求。响应中将包含您的身份验证 cookie。再次使用测试客户端使用该 cookie 进行经过身份验证的请求。根据正在测试的用户,相应地断言授权是否通过或失败。 - Chris Pratt
我做过并看过一些复杂的东西,因为我不知道这个,也没有和我一起工作的人知道。这将是我的集成测试迈出的巨大一步。谢谢! - Scott Hannen
除了这个答案的有用之外,特别感谢原始的“启动”根据环境进行分支的想法,并在那里设置测试数据库上下文,而不是在测试主机构建器服务中覆盖设置。 - dee zg

1
我认为问题在于依赖注入。在你的Startups.cs文件中,你可以找到类似的字符串:services.AddIdentity<AppUser, AppRole>().AddEntityFrameworkStores<AppDbContext>().AddDefaultTokenProviders(); 这意味着命名空间Microsoft.Extensions.DependencyInjection的魔力会在任何你想使用它的地方为你提供一个User-或RoleManger实例。例如,在InvitationsController中使用构造函数中的注入。
你可以尝试在测试类中注入UserManger并进行模拟。或者阅读类似的问题

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接