Xunit 和 Mock 数据与最小起订量

Xunit and Mock data with Moq

我是单元测试的新手,任何人都可以建议如何使用 xUnit 和 Moq 测试下面的 public 方法 (CreateUser),谢谢!

public async Task<bool> CreateUser(UserDTO newUser)
{
  newUser.CustomerId = _userResolverService.GetCustomerId();
  if (await CheckUserExists(newUser)) return false;
  var salt = GenerateSalt(10);
  var passwordHash = GenerateHash(newUser.Password, salt);

  await _usersRepository.AddAsync(new User()
  {
    Role = newUser.Role,
    CretedOn = DateTime.Now,
    CustomerId = newUser.CustomerId,
    Email = newUser.Email,
    FirstName = newUser.FirstName,
    LastName = newUser.LastName,
    PasswordHash = passwordHash,
    Salt = salt,
    UpdatedOn = DateTime.Now
  });

  return true;
}

下面是私有方法 检查用户是否仅存在 returns 现有用户数

private async Task<bool> CheckUserExists(UserDTO user)
    {
      var users = await _usersRepository.GetAllAsync();
      var userCount = users.Count(u => u.Email == user.Email);
      return userCount > 0;
    }

哈希生成

private static string GenerateHash(string input, string salt)
{
  var bytes = System.Text.Encoding.UTF8.GetBytes(input + salt);
  var sha256 = SHA256.Create();
  var hash = sha256.ComputeHash(bytes);

  return ByteArrayToString(hash);
}

盐世代

private static string GenerateSalt(int size)
{
  var rng = RandomNumberGenerator.Create();
  var buff = new byte[size];
  rng.GetBytes(buff);
  return Convert.ToBase64String(buff);
}



private static string ByteArrayToString(byte[] ba)
{
  var hex = new StringBuilder(ba.Length * 2);
  foreach (byte b in ba)
    hex.AppendFormat("{0:x2}", b);
  return hex.ToString();
}

谢谢, J

编辑 1

这是我目前所拥有的:

    [Fact]
        public async void CreateUser_True()
        {
          //arrange
          var dataSource = new Mock<IRepository<User, int>>();
          var userResolverService = new Mock<IUserResolverService>();
          var tokenService = new Mock<ITokenService>();

      var users = new List<User>();
      users.Add(new User()
      {
        Email = "test@test.com",
        CustomerId = 1
      });
      dataSource.Setup(m => m.GetAllAsync()).ReturnsAsync(users); // Error Here with converting async task to IEnumerable...

          var accountService = new AccountService(dataSource.Object,userResolverService.Object,tokenService.Object);


          //act


          //assert


        }

主要问题是我不知道如何模拟、设置私有方法 "CheckUserExists" 的行为,因为它正在调用 IRepository,它被模拟为 class,在这种情况下也是如此 return 我的 GetAllAsync 模拟设置来自这个私有方法?

编辑 2

public async Task<TEntity> AddAsync(TEntity entity)
{
  if (entity == null)
  {
    throw new ArgumentNullException(nameof(entity));
  }
  _entities.Add(entity);
  await _context.SaveChangesAsync();
  return entity;
}

编辑 3

[Fact]
public async void CreateUser_True()
{
  //arrange
  var users = new List<User>();
  users.Add(new User()
  {
    Email = "test@test.com",
    CustomerId = 1
  });
  _dataSource.Setup(m => m.GetAllAsync()).ReturnsAsync(users);
  _dataSource.Setup(m => m.AddAsync(It.IsAny<User>())).Returns<User>(Task.FromResult);
  var accountService = new AccountService(_dataSource.Object, _userResolverService.Object, _tokenService.Object);


  //act
  var result = await accountService.CreateUser(new UserDTO()
  {
    Email = "truetest@test.com"
  });

  var updatedUsersList = await _dataSource.Object.GetAllAsync();
  var usersCount = updatedUsersList.Count();

  //assert
  Assert.True(result);
  Assert.Equal(2, usersCount);

}

由于被测试的方法是异步的,您需要设置所有异步依赖项以允许方法流完成。至于私有方法,您想设置该方法中使用的任何依赖项的行为,在本例中是用户存储库。

[Fact]
public async Task CreateUser_True() {
    //arrange
    var usersRepository = new Mock<IRepository<User, int>>();
    var userResolverService = new Mock<IUserResolverService>();
    var tokenService = new Mock<ITokenService>();

    var user = new User() {
        Email = "test@test.com",
        CustomerId = 1
    };
    var users = new List<User>() { user };

    usersRepository.Setup(_ => _.GetAllAsync()).ReturnsAsync(users);
    usersRepository.Setup(_ => _.AddAsync(It.IsAny<User>()))
        .Returns<User>(arg => Task.FromResult(arg)) //<-- returning the input value from task.
        .Callback<User>(arg => users.Add(arg)); //<-- use call back to perform function
    userResolverService.Setup(_ => _.GetCustomerId()).Returns(2);
    var accountService = new AccountService(usersRepository.Object, userResolverService.Object, tokenService.Object);

    //act
    var actual = await accountService.CreateUser(new UserDto { 
        Email = "email@example.com",
        Password = "monkey123",
        //...other code removed for brevity
    });

    //assert
    Assert.IsTrue(actual);
    Assert.IsTrue(users.Count == 2);
    Assert.IsTrue(users.Any(u => u.CustomerId == 2);
}

继续阅读 Moq Quickstart 以更好地了解如何使用模拟框架。

@sziszu 据我正确理解,您可以为 public 方法的单元测试设置模拟。这是解决方案。

我重写了你的代码,我的项目结构看起来像 visual studio project structure

这是我的代码片段

用户操作Class

    public class UserOperation
    {
       #region Fields
    
       private readonly IUserResolverService _userResolverService;
       private readonly IUsersRepository _usersRepository;

       #endregion

       #region Constructor

       public UserOperation(IUserResolverService userResolverService, IUsersRepository usersRepository)
       {
          _userResolverService = userResolverService;
          _usersRepository = usersRepository;
       }

       #endregion

       #region Public Methods

       public async Task<bool> CreateUser(UserDTO newUser)
       {
          newUser.CustomerId = _userResolverService.GetCustomerId();
          if (await CheckUserExists(newUser)) return false;
          var salt = GenerateSalt(10);
          var passwordHash = GenerateHash(newUser.Password, salt);

          await _usersRepository.AddAsync(new User
          {
              Role = newUser.Role,
              CretedOn = DateTime.Now,
              CustomerId = newUser.CustomerId,
              Email = newUser.Email,
              FirstName = newUser.FirstName,
              LastName = newUser.LastName,
              PasswordHash = passwordHash,
              Salt = salt,
              UpdatedOn = DateTime.Now
          });

          return true;
       }

       #endregion

       #region PrivateMethods

       private async Task<bool> CheckUserExists(UserDTO user)
       {
           var users = await _usersRepository.GetAllAsync();
           var userCount = users.Count(u => u.Email == user.Email);
           return userCount > 0;
       }

       private static string GenerateHash(string input, string salt)
       {
           var bytes = Encoding.UTF8.GetBytes(input + salt);
           var sha256 = SHA256.Create();
           var hash = sha256.ComputeHash(bytes);

           return ByteArrayToString(hash);
       }

       private static string GenerateSalt(int size)
       {
           var rng = RandomNumberGenerator.Create();
           var buff = new byte[size];
           rng.GetBytes(buff);
           return Convert.ToBase64String(buff);
       }

       private static string ByteArrayToString(byte[] ba)
       {
           var hex = new StringBuilder(ba.Length * 2);
           foreach (byte b in ba)
           hex.AppendFormat("{0:x2}", b);
           return hex.ToString();
       }

       #endregion
   }

以下是我的存储库和服务。

   public interface IUsersRepository
   {
       Task AddAsync(User user);
       Task<ICollection<User>> GetAllAsync();
   }
   
   public class UsersRepository : IUsersRepository
   {
       public Task AddAsync(User user)
       {
            //Code for adding user
       }

       public Task<ICollection<User>> GetAllAsync()
       {
           //Code for get all user from DB
       }
   }

   public interface IUserResolverService
   {
       string GetCustomerId();
   }

   public class UserResolverService : IUserResolverService
   { 
       public string GetCustomerId()
       {
           //Code for Getting customerID.
       }
   }

在这里您可以使用 mock

进行测试
  public class UserOperationTest
  {
    private readonly UserOperation _sut;
    private readonly IUserResolverService _userResolverService;
    private readonly IUsersRepository _userRepository;

    public UserOperationTest()
    {
        _userResolverService = Substitute.For<IUserResolverService>();
        _userRepository = Substitute.For<IUsersRepository>();

        _sut = new UserOperation(_userResolverService, _userRepository);
    }

    [Fact]
    public void CreateUser_SuccessWithMock()
    {
        // Arrange
        var userDto = new UserDTO
        {
            Email = "ThirdUserUserEmail.Com"
        };

        var userList = new List<User>()
        {
            new User{CustomerId = "1", Email = "FirstUserEmail.Com"},
            new User{CustomerId = "2", Email = "SecondUserEmail.Com"}
        };
        _userResolverService.GetCustomerId().Returns("3");
        _userRepository.GetAllAsync().Returns(userList);
        _userRepository.When(x => x.AddAsync(Arg.Any<User>())).Do(x =>
        {
            userList.Add(new User {Email = userDto.Email, CustomerId = userDto.CustomerId});
        });
        
        //Act
        var result = _sut.CreateUser(userDto);

        // Assert
        result.Result.Should().BeTrue();
        _userResolverService.Received(1).GetCustomerId();
        _userRepository.Received(1).GetAllAsync();
        _userRepository.Received(1).AddAsync(Arg.Any<User>());
        userList.Count.Should().Be(3);
    }
}