Asp.net Core Identity 单元测试控制器操作

Asp.net Core Identity unit test controller actions

我在确定如何测试以及测试什么时遇到问题。

我有一个注入 UserManager 并调用 CreateAsync 方法来创建新用户的控制器。

我不想测试身份用户管理器,因为它显然已经过全面测试。我想做的是测试控制器是否通过正确的路径运行(在我的例子中,有 3 条路径,发送回模型状态错误、身份响应错误或简单字符串的响应)

我是否应该尝试创建用户管理器的模拟以创建我的测试(我不确定如何将用户管理器设置为模拟依赖项) 其次,我如何设置条件来验证控制器是否已采用给定路径。

我正在使用 xUnitMoq

[Route("api/[controller]")]
public class MembershipController : BaseApiController
{
    private UserManager<ApplicationUser> _userManager;

    public MembershipController(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    [HttpGet("RegisterNewUser")]
    public HttpResponseMessage RegisterNewUser([FromBody] NewUserRegistration user)
    {
        if (ModelState.IsValid)
        {
            ApplicationUser newUser = new ApplicationUser();
            newUser.UserName = user.username;
            newUser.Email = user.password;
            IdentityResult result = _userManager.CreateAsync(newUser, user.password).Result;

            if (result.Errors.Count() > 0)
            {
                var errors = new IdentityResultErrorResponse().returnResponseErrors(result.Errors);
                return this.WebApiResponse(errors, HttpStatusCode.BadRequest);
            }
        }
        else
        {
            var errors = new ViewModelResultErrorResponse().returnResponseErrors(ModelState);
            return this.WebApiResponse(errors, HttpStatusCode.BadRequest);
        }

        return this.WebApiResponse(
                    "We have sent a valifation email to you, please click on the verify email account link.",
                    HttpStatusCode.OK);
    }
}

在我的单元测试中,我有以下内容来测试快乐路径场景

    [Fact]
    public void RegisterNewUser_ReturnsHttpStatusOK_WhenValidModelPosted()
    {
        var mockStore = new Mock<IUserStore<ApplicationUser>>();
        var mockUserManager = new Mock<UserManager<ApplicationUser>>(mockStore.Object, null, null, null, null, null, null, null, null);

        ApplicationUser testUser = new ApplicationUser { UserName = "user@test.com" };

        mockStore.Setup(x => x.CreateAsync(testUser, It.IsAny<CancellationToken>()))
           .Returns(Task.FromResult(IdentityResult.Success));

        mockStore.Setup(x => x.FindByNameAsync(testUser.UserName, It.IsAny<CancellationToken>()))
                    .Returns(Task.FromResult(testUser));


        mockUserManager.Setup(x => x.CreateAsync(testUser).Result).Returns(new IdentityResult());

        MembershipController sut = new MembershipController(mockUserManager.Object);
        var input = new NewUserInputBuilder().Build();
        sut.RegisterNewUser(input);

    }

其中"input" in sut.RegisterNewUser(输入);指的是一个助手class,它构建了控制器操作所需的视图模型:

public class NewUserInputBuilder
{
    private string username { get; set; }
    private string password { get; set; }
    private string passwordConfirmation { get; set; }
    private string firstname { get; set; }
    private string lastname { get; set; }

    internal NewUserInputBuilder()
    {
        this.username = "user@test.com";
        this.password = "password";
        this.passwordConfirmation = "password";
        this.firstname = "user";
        this.lastname = "name";
    }

    internal NewUserInputBuilder WithNoUsername()
    {
        this.username = "";
        return this;
    }

    internal NewUserInputBuilder WithMisMatchedPasswordConfirmation()
    {
        this.passwordConfirmation = "MismatchedPassword";
        return this;
    }

    internal NewUserRegistration Build()
    {
        return new NewUserRegistration
        { username = this.username, password = this.password,
            passwordConfirmation = this.passwordConfirmation,
            firstname = this.firstname, lastname = this.lastname
        };
    }
} 

我的目标是通过测试强制满足 3 个条件:

  1. 创建有效的视图模型和return成功消息
  2. 创建一个有效的视图模型但是 return 一个 IdentityResponse 错误(例如用户存在)被转换为
  3. 创建无效的视图模型和 returns 模型状态错误

使用抽象 class 处理错误,其中 return 是一个 json 对象 控制器的基础 class 只是为 return.

构造了一个 HttpResponseMessage

基本上我想检查是否通过强制测试沿着模型状态错误路径、identityresult.errors 路径调用了正确的错误响应 class 并且可以实现快乐路径。

那么我的计划是单独测试错误响应 classes。

希望这些足够详细了。

被测方法应该是异步的,而不是使用阻塞调用,即 .Result

[HttpGet("RegisterNewUser")]
public async Task<HttpResponseMessage> RegisterNewUser([FromBody] NewUserRegistration user) {
    if (ModelState.IsValid) {
        var newUser = new ApplicationUser() {
            UserName = user.username,
            Email = user.password
        };
        var result = await _userManager.CreateAsync(newUser, user.password);
        if (result.Errors.Count() > 0) {
            var errors = new IdentityResultErrorResponse().returnResponseErrors(result.Errors);
            return this.WebApiResponse(errors, HttpStatusCode.BadRequest);
        }
    } else {
        var errors = new ViewModelResultErrorResponse().returnResponseErrors(ModelState);
        return this.WebApiResponse(errors, HttpStatusCode.BadRequest);
    }

    return this.WebApiResponse(
                "We have sent a valifation email to you, please click on the verify email account link.",
                HttpStatusCode.OK);
}

审查 Happy path 场景和测试方法表明不需要 设置 UserStore 因为测试将直接覆盖用户管理器虚拟成员。

请注意,测试也已设为异步。

  1. Create a valid viewmodel and return a success message
[Fact]
public async Task RegisterNewUser_ReturnsHttpStatusOK_WhenValidModelPosted() {
    //Arrange
    var mockStore = Mock.Of<IUserStore<ApplicationUser>>();
    var mockUserManager = new Mock<UserManager<ApplicationUser>>(mockStore, null, null, null, null, null, null, null, null);

    mockUserManager
        .Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()))
        .ReturnsAsync(IdentityResult.Success);

    var sut = new MembershipController(mockUserManager.Object);
    var input = new NewUserInputBuilder().Build();

    //Act
    var actual = await sut.RegisterNewUser(input);

    //Assert
    actual
        .Should().NotBeNull()
        .And.Match<HttpResponseMessage>(_ => _.IsSuccessStatusCode == true);        
}
  1. Create a valid viewmodel but returns a IdentityResponse error (eg. user exists) which gets converted

为此,您只需将模拟设置为 return 有错误的结果。

[Fact]
public async Task RegisterNewUser_ReturnsHttpStatusBadRequest_WhenViewModelPosted() {
    //Arrange

    //...code removed for brevity

    mockUserManager
        .Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()))
        .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "test"}));

    //...code removed for brevity

    //Assert
    actual
        .Should().NotBeNull()
        .And.Match<HttpResponseMessage>(_ => _.StatusCode == HttpStatusCode.BadRequest);
}

  1. Create an invalid viewmodel and returns Modelstate errors

您只需要设置控制器的模型状态使其无效即可。

[Fact]
public async Task RegisterNewUser_ReturnsHttpStatusBadRequest_WhenInvalidModelState() {
    //Arrange
    var mockStore = Mock.Of<IUserStore<ApplicationUser>>();
    var mockUserManager = new Mock<UserManager<ApplicationUser>>(mockStore, null, null, null, null, null, null, null, null);

    var sut = new MembershipController(mockUserManager.Object);
    sut.ModelState.AddModelError("", "invalid data");
    var input = new NewUserInputBuilder().Build();

    //Act
    var actual = await sut.RegisterNewUser(input);

    //Assert
    actual
        .Should().NotBeNull()
        .And.Match<HttpResponseMessage>(_ => _.StatusCode == HttpStatusCode.BadRequest);    
}

FluentAssertions 用于执行所有断言。你可以很容易地使用 Assert.* API。

这应该足以帮助您解决上述问题。

如果您不想测试用户管理器,这里有一个使用 NUnit 的简单方法(您可以使用 xUnit 做类似的事情)。 (我还展示了如何使用可用于设置模拟数据的 in-memory 数据库将 DbContext 传递到同一个控制器)

    private DbContextOptions<MyContextName> options;

    [OneTimeSetUp]
    public void SetUp()
    {
        options = new DbContextOptionsBuilder<MyContextName>()
            .UseInMemoryDatabase(databaseName: "MyDatabase")
            .Options;

        // Insert seed data into the in-memory mock database using one instance of the context
        using (var context = new MyContextName(options))
        {
            var testWibble = new Wibble { MyProperty = 1, MyOtherProperty = 2 ... };
            context.wibbles.Add(testWibble);

            context.SaveChanges();
        }
    }


    [Test]
    public void Some_TestMethod()
    {
        // Use a clean instance of the context to run the test
        using (var context = new MyDbContext(options))
        {
            var store = new UserStore<MyUserType>(context);
            var userManager = new UserManager<MyUserType>(store, null, null, null, null, null, null, null, null);

            MyController MyController = new MyController(userManager, context);

            ... test the controller
        }
    }