【问题标题】:Mocking in Unit Tests单元测试中的模拟
【发布时间】:2013-09-05 20:00:03
【问题描述】:

我正在尝试测试以下CategoryServiceAddCategory

我的问题是我很难理解要模拟/伪造什么。

我在测试中的尝试是在底部。

我正在使用 MOQ、xUnit 和 FluentAssertions。

我正在为验证器使用 FluentValidation。

分类服务

public class CategoryService : ValidatingServiceBase, ICategoryService
{
    private readonly IUnitOfWork unitOfWork;
    private readonly IRepository<Category> categoryRepository;
    private readonly IRepository<SubCategory> subCategoryRepository;
    private readonly IValidationService validationService;

    public CategoryService(
        IUnitOfWork unitOfWork,
        IRepository<Category> categoryRepository,
        IRepository<SubCategory> subCategoryRepository,
        IValidationService validationService)
        : base(validationService)
    {
        this.unitOfWork = unitOfWork;
        this.categoryRepository = categoryRepository;
        this.subCategoryRepository = subCategoryRepository;
        this.validationService = validationService;
    }

    public bool AddCategory(Category category)
    {
        var validationResult = validationService.Validate(category);

        if (!validationResult.IsValid)
        {
            return false;
        }
        else
        {
            categoryRepository.Add(category);
            return true;
        }
    }

    public bool DoesCategoryExist(string categoryName)
    {
        return categoryRepository.Query().SingleOrDefault(x => x.Name == categoryName) != null;
    }
}

验证服务

public class ValidationService : ServiceBase, IValidationService
{
    private readonly IValidatorFactory validatorFactory;

    public ValidationService(IValidatorFactory validatorFactory)
    {
        Enforce.ArgumentNotNull(validatorFactory, "validatorFactory");

        this.validatorFactory = validatorFactory;
    }

    public ValidationResult Validate<TEntity>(TEntity entity) where TEntity : class
    {
        var validator = validatorFactory.GetValidator<TEntity>();
        return validator.Validate(entity);
    }
}

验证器工厂

public class ValidatorFactory : IValidatorFactory
{
    public IValidator GetValidator(Type type)
    {
        Enforce.ArgumentNotNull(type, "type");

        return DependencyResolver.Current.GetService(typeof(IValidator<>).MakeGenericType(type)) as IValidator;
    }

    public IValidator<T> GetValidator<T>()
    {
        return DependencyResolver.Current.GetService<IValidator<T>>();
    }
}

类别验证器

public class CategoryValidator : AbstractValidator<Category>
{
    public CategoryValidator(ICategoryService service)
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .Must((category, name) =>
            {
                return service.DoesCategoryExist(name);
            });
    }
}

单元测试尝试

[Fact]
    public void AddCategory_Should_ReturnTrue()
    {
        var category = new Category() { Name = "Cat1" };

        var unitOfWork = new Mock<IUnitOfWork>();
        var categoryRepo = new Mock<IRepository<Category>>();
        var subCategoryRepo = new Mock<IRepository<SubCategory>>();

        var mockCategoryService = new Mock<ICategoryService>();
        var categoryValidator = new CategoryValidator(mockCategoryService.Object);

        var validatorFactory = new Mock<IValidatorFactory>();
        validatorFactory.Setup(x => x.GetValidator<CategoryValidator>()).Returns(categoryValidator as IValidator<CategoryValidator>);

        var validationService = new ValidationService(validatorFactory.Object);

        var categoryService = new CategoryService(
            unitOfWork.Object,
            categoryRepo.Object,
            subCategoryRepo.Object,
            validationService);

        categoryService.AddCategory(category);
    }

【问题讨论】:

    标签: unit-testing moq fluentvalidation xunit.net


    【解决方案1】:

    对于 AddCategory 方法,我认为您真的只需要两个模拟,一个用于 ValidationService,一个用于 CategoryRepository,因为其他依赖项不在该函数中执行,因此是无关的

    (当然,如果你的 ctor 抛出空参数,情况可能会有所不同,但在这种情况下,我认为你没问题 - 尽管你可能会考虑在未来添加这些检查 :)

    无论如何,作为迂腐的,我几乎倾向于为此函数编写两个(或更多——也许一个用于 null 输入以验证它抛出或返回 false 或其他)“单元”测试;

    • 一个验证给定无效类别的函数返回false,
    • 一个用于验证给定有效类别的函数会调用 CategoryRepository 依赖项上的 Add。

    所以它看起来像这样(抱歉,这是使用 MSTest 语法,因为我不熟悉 xUnit,但它是相同的想法)。也没有在下面测试错别字等:)

    public void AddCategory_InvalidCategory_ShouldReturnFalse()
    {
    //Arrange
       var mockValidator = new Mock<IValidator>();
    //no matter what we pass to the validator, it will return false
       mockValidator.Setup(v=>v.Validate(It.IsAny<Category>()).Returns(false);
       var sut= new CategoryService(null,null,null,mockValidator.Object);
       bool expected = false;
    
    //ACT
      bool actual = sut.AddCategory(new Category());
    
    //ASSERT
      Assert.AreEqual(expected,actual,"Validator didn't return false as expected");
    
    }
    
    public void AddCategory_ValidCategory_ShouldCallRepositoryAdd()
    {
    //Arrange
       var mockValidator = new Mock<IValidator>();
    //no matter what we pass to the validator, it will return true
       mockValidator.Setup(v=>v.Validate(It.IsAny<Category>()).Returns(true);
       var mockRepo = new Mock<IRepository<SubCategory>>();
       mockRepo.Setup(r=>r.Add(It.IsAny<Category>())); //do not know or care what happens as this is a void method.
       var sut= new  CategoryService(null,mockRepo.Object,null,mockValidator.Object);
    bool expected = false;
    
    //ACT
        bool actual = sut.AddCategory(new Category());
    
    //ASSERT
       mockRepo.Verify(r=>r.Add(It.IsAny<Category>(),Times.Exactly(1),"Repo ADD method not called or called too many times, etc");
       Assert.AreEqual(expected,actual,"Add was called BUT the AddCategoryMethod didn't return true as expected"); //and of course you could be totally pedantic and create a new test method for that last assert ;)
    }
    

    我赞成这种方法的原因是因为它迫使您考虑被测方法的行为,并确保您不涉及任何未测试的依赖项,而且这意味着您的测试方法只能精确地创建他们需要什么才能运行测试(当然,您可以创建一些设置/拆卸助手来为您预先创建这些模拟);

    当然,您可以将以上所有内容放在一个方法中,但为了节省一些 LOC,我希望您同意使用两个单独的测试来验证两个单独的行为是一种更稳健的方法。

    只是我的 2c。希望有帮助!

    【讨论】:

    • 那么在我的AddCategory服务方法中,我不需要测试方法的内部工作原理吗?喜欢ValidationServiceValidators?只需模拟 ValidationService 以返回有效与否,然后基于此测试 AddCategory 方法的作用?我只需要测试AddCategory 方法返回的结果吗?如果它是一个 void 方法,那会改变你测试它的方式吗?
    • @Sam - 或多或少。您通过行为检查间接测试内部运作。关键问题是 - 我需要断言的最小 组事实是什么,以确保该方法按我设计的方式运行?我在上面的回答中注意到,我假设 Validator 返回了 bool - 它返回一个对象,但您可以告诉模拟返回模拟验证状态对象本身,原理是一样的。
    猜你喜欢
    • 2019-12-13
    • 1970-01-01
    • 2013-02-22
    • 2011-04-11
    • 2020-05-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多