单元测试依赖于UserManager的控制器的最佳实践 ?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单元测试依赖于UserManager的控制器的最佳实践 ?相关的知识,希望对你有一定的参考价值。
我有一个带有以下签名的控制器:
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private ILogger<UsersController> _logger;
private readonly UserManager<IdentityUser> _usermanager;
public UsersController(ILogger<UsersController> logger, UserManager<IdentityUser> usermanager)
{
_usermanager = usermanager;
_logger = logger;
}
[HttpGet("{_uniqueid}")]
public async Task<ObjectResult> GetUser(string _uniqueid)
{
//Retrieve the object
try
{
var user = await _usermanager.FindByIdAsync(uniqueid);
var model = JsonConvert.DeserializeObject<GetUserModel>(user.ToString());
return new ObjectResult(JsonConvert.SerializeObject(model));
}
catch(CustomIdentityNotFoundException e)
{
return new BadRequestObjectResult(("User not found: {0}", e.Message));
}
}
}
现在我的单元测试看起来像这样:
public class UsersUnitTests
{
public UsersController _usersController;
private UserManager<IdentityUser> _userManager;
public UsersUnitTests()
{
_userManager = new MoqUserManager<IdentityUser>();
_usersController = new UsersController((new Mock<ILogger<UsersController>>()).Object, _userManager);
}
[Fact]
public async Task GetUser_ReturnsOkObjectResult_WhenModelStateIsValid()
{
//Setup
//Test
ObjectResult response = await _usersController.GetUser("realuser");
//Assert
//Should receive 200 and user data content body
response.StatusCode.Should().Be((int)System.Net.HttpStatusCode.OK);
response.Value.Should().NotBeNull();
}
}
以及Moq的课程:
public class MoqUserManager<T> : UserManager<IdentityUser>
{
public MoqUserManager(IUserStore<IdentityUser> store, IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<IdentityUser> passwordHasher, IEnumerable<IUserValidator<IdentityUser>> userValidators,
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<IdentityUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
}
public MoqUserManager()
: base((new MoqUserStore().Store), new Mock<IOptions<IdentityOptions>>().Object,
new Mock<IPasswordHasher<IdentityUser>>().Object, new Mock<IEnumerable<IUserValidator<IdentityUser>>>().Object,
new Mock<IEnumerable<IPasswordValidator<IdentityUser>>>().Object, new Mock<ILookupNormalizer>().Object,
new Mock<IdentityErrorDescriber>().Object, new Mock<IServiceProvider>().Object, new Mock<ILogger<UserManager<IdentityUser>>>().Object)
{
}
}
public class MoqUserStore : IdentityUserStore
{
private Mock<IdentityUserStore> _store;
public MoqUserStore()
:base(new Mock<IdentityDbContext>().Object, new Mock<ILogger<IdentityUserStore>>().Object, null)
{
_store = new Mock<IdentityUserStore>(new Mock<IdentityDbContext>().Object, new Mock<ILogger<IdentityUserStore>>().Object, null);
_store.Setup(x => x.FindByIdAsync("realuser", default(CancellationToken))).Returns(Task.Run(() => new IdentityUser("realuser")));
_store.Setup(x => x.FindByIdAsync("notrealuser", default(CancellationToken))).Throws(new CustomIdentityNotFoundException());
_store.Setup(x => x.CreateAsync(new IdentityUser("realuser"), default(CancellationToken))).Returns(Task.Run(() => IdentityResult.Success));
}
public IdentityUserStore Store { get => _store.Object; }
}
调用reference not set to an instance of an object
构造函数时,我得到MoqUserManager
错误。
我的问题是:对于依赖于UserManager
和/或SignInManager
的这些类型的控制器进行单元测试,最好的做法是什么(我会满足于工作,但是很难找到天堂)?什么是模拟UserStore
依赖的简单可重复的方法?
我想到了DI模型和我的控制器的依赖关系。我只需要来自UserManager
的一些方法,所以我理论上从UserManager
中删除对UsersController
的依赖,并用一些实现我需要从UserManager
签名的接口替换它。让我们称之为接口IMYUserManager
:
public interface IMYUserManager
{
Task<IdentityUser> FindByIdAsync(string uniqueid);
Task<IdentityResult> CreateAsync(IdentityUser IdentityUser);
Task<IdentityResult> UpdateAsync(IdentityUser IdentityUser);
Task<IdentityResult> DeleteAsync(IdentityUser result);
}
接下来,我需要创建一个既来自UserManager
又实现IMYUserManager
的类。这里的想法是从接口实现方法将简单地成为派生类的覆盖,这样我绕过FindByIdAsync
(和其余的)被标记为扩展方法并需要包装在静态类中。这是MyUserManager
:
public class MYUserManager : UserManager<IdentityUser>, IMYUserManager
{
public MYUserManager(IUserStore<IdentityUser> store, IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<IdentityUser> passwordHasher, IEnumerable<IUserValidator<IdentityUser>> userValidators,
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<IdentityUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
}
public override Task<IdentityUser> FindByIdAsync(string userId)
{
return base.FindByIdAsync(userId);
}
//Removed other overridden methods for brevity; They also call the base class method
}
快到家。接下来,我自然更新了UsersController
以使用IMYUserManager
界面:
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private ILogger<UsersController> _logger;
private readonly IMYUserManager _usermanager;
public UsersController(ILogger<UsersController> logger, IMYUserManager
usermanager)
{
_usermanager = usermanager;
_logger = logger;
}
}
而且,自然之后我必须为所有想要享用盛宴的人提供服务容器的依赖:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IMYUserManager, MYUserManager>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
最后,在验证实际构建之后,我更新了测试类:
public class UsersControllerTests
{
public UsersController _usersController;
private Mock<IMYUserManager> _userManager;
public UsersControllerTests()
{
_userManager = new Mock<IMYUserManager>();
_usersController = new UsersController((new Mock<ILogger<UsersController>>
()).Object, _userManager.Object);
}
[Fact]
public async Task GetUser_ReturnsOkObjectResult_WhenModelStateIsValid()
{
//Setup
_userManager.Setup(x => x.FindByIdAsync("realuser"))
.Returns(Task.Run(() => new IdentityUser("realuser","realuser1")));
_usersController.ModelState.Clear();
//Test
ObjectResult response = await _usersController.GetUser("realuser");
//Assert
//Should receive 200 and user data content body
response.StatusCode.Should().Be((int)System.Net.HttpStatusCode.OK);
response.Value.Should().NotBeNull();
}
}
什么使这成为一个好的解决方案
几件事:
从UserManager
中删除对UsersController
的依赖性与DI模型一致。抽象出依赖关系(因此抽象出扩展方法之类的实现细节)并使它们不仅可以被模拟,而且可用于整个IServiceCollection
意味着当我需要为用户管理器实现另一种方法时,我只有3个非常简单的步骤:
- 将方法签名添加到
IMYUserManager
- 重写该方法并在
MYUserManager
中调用基类实现 - 在单元测试中模拟新的依赖
我可能会重新审视服务的范围,我选择AddScoped()
只是为了证明这个概念,但性能和业务要求将选择是否保持不变。
以上是关于单元测试依赖于UserManager的控制器的最佳实践 ?的主要内容,如果未能解决你的问题,请参考以下文章