有这样一种方法可以从数据库中获取一些数据:
public async Task<T> GetByFilter(Func<IQueryable<T>, IQueryable<T>>? queryModifier, CancellationToken cancellationToken = default)
{
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(GET_BY_FILTER_AWAITING));
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token).Token;
IQueryable<T> query = _dbSet;
if (queryModifier is not null)
query = queryModifier(query);
return await query.FirstOrDefaultAsync(cancellationToken);
}
catch (OperationCanceledException)
{
throw new OperationCanceledException(REQUEST_TIMED_OUT);
}
catch (Exception ex)
{
_logger.LogCritical(ex.ToString(), nameof(_context), nameof(_dbSet));
throw new OperationCanceledException(Message.ERROR);
}
}
它将现有 LINQ 查询的修改委托作为参数:
Func<IQueryable<T>, IQueryable<T>>? queryModifier
接下来,在需要测试的方法中,这个函数用于接收用户的电子邮件:
var user = await userRepository.GetByFilter(query => query.Where(u => u.email.Equals(email.ToLowerInvariant())));
下面是需要测试的方法的完整代码:
public async Task<IActionResult> RecoveryAccount([FromQuery] string email)
{
try
{
var user = await userRepository.GetByFilter(query => query.Where(u => u.email.Equals(email.ToLowerInvariant())));
if (user is null)
return StatusCode(404, new { message = Message.NOT_FOUND });
string token = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString() + generate.GenerateKey();
await recoveryHelper.CreateTokenTransaction(user, token);
await emailSender.SendMessage(new EmailDto
{
username = user.username,
email = email,
subject = EmailMessage.RecoveryAccountHeader,
message = EmailMessage.RecoveryAccountBody + $"{fileManager.GetReactAppUrl()}/auth/recovery?token={token}"
});
await redisCache.DeteteCacheByKeyPattern($"{ImmutableData.NOTIFICATIONS_PREFIX}{user.id}");
return StatusCode(201, new { message = Message.EMAIL_SENT });
}
// Handling some exceptions, doesn't make sense to specify in the question
catch (Exception ex)
{
return StatusCode(500, new { message = ex.Message });
}
}
测试时,我需要使用特定的 Func
我这样做:
// Creating Mock Object UserRepository:
var userRepositoryMock = new Mock<IRepository<UserModel»();
// Creating specific Func
Func<IQueryable<UserModel>, IQueryable<UserModel» query = query => query.Where(u => u.email.Equals(email.ToLowerInvariant()));
// Mock my method with my Func
userRepositoryMock.Setup(x => x.GetByFilter(query, CancellationToken.None))
.ReturnsAsync(user);
但是作为模拟的结果,我没有得到预期的user对象:.ReturnsAsync(user),但是null
我认为这是因为比较了对委托的引用,而不是委托的值,所以不存在巧合,我不明白如何解决这个问题。
我注释掉了那些可能与问题没有太大关系的地方。
接下来是测试本身,其中需要模拟具有特定 Func 的方法:
[Fact]
public async Task RecoveryAccount_Success()
{
var id = 1;
var email = "[email protected]";
var username = "air2921";
Func<IQueryable<UserModel>, IQueryable<UserModel>> query = query => query.Where(u => u.email.Equals(email.ToLowerInvariant()));
var user = new UserModel
{
id = id,
username = username,
email = email
};
var userRepositoryMock = new Mock<IRepository<UserModel>>();
//var generateMock = new Mock<IGenerate>();
//var emailSenderMock = new Mock<IEmailSender>();
//var redisCacheMock = new Mock<IRedisCache>();
//var fileManagerMock = new Mock<IFileManager>();
//var recoveryServiceMock = new Mock<IRecoveryHelpers>();
userRepositoryMock.Setup(x => x.GetByFilter(query, CancellationToken.None))
.ReturnsAsync(user);
//generateMock.Setup(x => x.GenerateKey()).Returns("8ifrnDa8a9nabJDfjTrfXsgfVIhCYGrZbN5HdtX0dK8=");
//fileManagerMock.Setup(x => x.GetReactAppUrl()).Returns(string.Empty);
var recoveryController = new RecoveryController(/*recoveryServiceMock.Object, null,*/ userRepositoryMock.Object/*,*/
/*null, emailSenderMock.Object, redisCacheMock.Object, fileManagerMock.Object, generateMock.Object*/);
var result = await recoveryController.RecoveryAccount(email);
Assert.IsType<ObjectResult>(result);
var objectResult = (ObjectResult)result;
Assert.Equal(201, objectResult.StatusCode);
//recoveryServiceMock.Verify(x => x.CreateTokenTransaction(user, It.Is<string>(q => q.Length >= 112)), Times.Once);
//emailSenderMock.Verify(x => x.SendMessage(It.Is<EmailDto>(e => e.username == username && e.email == email)), Times.Once);
//redisCacheMock.Verify(x => x.DeteteCacheByKeyPattern($"{ImmutableData.NOTIFICATIONS_PREFIX}{id}"), Times.Once);
}
我的建议是创建一个自定义类来模拟您的存储库。类似于
MemoryRepository
,使用列表或字典作为后备存储,但在其他方面满足 IRepository
的接口。
编写单元测试时,您应该测试方法的“接口”。如果您的测试只是方法应调用的事物列表,那么您的测试可能会很脆弱,并且实现中的任何更改都可能会破坏您的测试。提供更完整的依赖项实现可能有助于避免这种脆弱性。
并不是说模拟框架有什么不好。但它们可能不是各种嘲笑的最佳工具。
请注意,正确重现数据库的行为可能很困难。并非所有编译的查询都可以转换为 SQL,并且某些行为将取决于所使用的数据库引擎。 EF 内存数据库 可能对某些类型的测试有用。使用数据库的实际实例对于其他测试很有用,在这些测试中,正确性比运行时之类的东西更重要。