Asp.net Core Identity 单元测试控制器操作
Asp.net Core Identity unit test controller actions
我在确定如何测试以及测试什么时遇到问题。
我有一个注入 UserManager
并调用 CreateAsync
方法来创建新用户的控制器。
我不想测试身份用户管理器,因为它显然已经过全面测试。我想做的是测试控制器是否通过正确的路径运行(在我的例子中,有 3 条路径,发送回模型状态错误、身份响应错误或简单字符串的响应)
我是否应该尝试创建用户管理器的模拟以创建我的测试(我不确定如何将用户管理器设置为模拟依赖项)
其次,我如何设置条件来验证控制器是否已采用给定路径。
我正在使用 xUnit 和 Moq。
[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 个条件:
- 创建有效的视图模型和return成功消息
- 创建一个有效的视图模型但是 return 一个 IdentityResponse 错误(例如用户存在)被转换为
- 创建无效的视图模型和 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
因为测试将直接覆盖用户管理器虚拟成员。
请注意,测试也已设为异步。
- 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);
}
- 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);
}
而
- 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
}
}
我在确定如何测试以及测试什么时遇到问题。
我有一个注入 UserManager
并调用 CreateAsync
方法来创建新用户的控制器。
我不想测试身份用户管理器,因为它显然已经过全面测试。我想做的是测试控制器是否通过正确的路径运行(在我的例子中,有 3 条路径,发送回模型状态错误、身份响应错误或简单字符串的响应)
我是否应该尝试创建用户管理器的模拟以创建我的测试(我不确定如何将用户管理器设置为模拟依赖项) 其次,我如何设置条件来验证控制器是否已采用给定路径。
我正在使用 xUnit 和 Moq。
[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 个条件:
- 创建有效的视图模型和return成功消息
- 创建一个有效的视图模型但是 return 一个 IdentityResponse 错误(例如用户存在)被转换为
- 创建无效的视图模型和 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
因为测试将直接覆盖用户管理器虚拟成员。
请注意,测试也已设为异步。
- 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);
}
- 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);
}
而
- 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
}
}