【问题标题】:Mocking domain entity class' properties in unit tests: methods, abstract classes or virtual properties? [closed]在单元测试中模拟域实体类的属性:方法、抽象类还是虚拟属性? [关闭]
【发布时间】:2020-01-25 22:31:14
【问题描述】:

我有一个代表领域实体的类​​,而这个类实现任何接口。让我们考虑一些简单的事情:

public class DomainEntity
{
    public DomainEntity(string name) { Name = name; }
    public string Name { get; private set; }
}

我正在测试其他一些课程。它有一个接受我的DomainEntity 作为参数的方法,该方法访问Name 属性。例如:

public class EntityNameChecker : IEntityNameChecker
{
    public bool IsDomainEntityNameValid(DomainEntity entity)
    {
        if (entity.Name == "Valid") { return true; }

        return false;
    }
}

我必须模拟我的DomainEntity 进行测试。我使用NSubstitute 作为我的模拟库(它不允许模拟非虚拟/非抽象属性)。

所以,在不添加接口的情况下(a-la IDomainEntity)我有三个选项来模拟剩余的属性值。

  1. Name 属性设为虚拟:

    public class DomainEntity
    {
        public DomainEntity(string name) { Name = name; }
        public virtual string Name { get; }
    }
    

    这里的缺点是我不能再将我的DomainEntity 类设为sealed,这意味着任何消费者都可以从它继承并覆盖我的Name 属性。

  2. 创建一个抽象的“基”类,并将基类类型作为参数类型:

    public abstract class DomainEntityBase
    {
        protected abstract string Name { get; private protected set; }
    }
    
    public sealed class DomainEntity : DomainEntityBase
    {
        public DomainEntity(string name) { Name = name; }
        protected override string Name { get; private protected set; }
    }
    
    public class EntityNameChecker : IEntityNameChecker
    {
        public bool IsDomainEntityNameValid(DomainEntityBase entity)
        {
            if (entity.Name == "Valid") { return true; }
    
            return false;
        }
    }
    

    这里的缺点是过于复杂。这基本上将抽象类变成了某种接口。

  3. 不是直接访问Name 属性,而是将Name getter 转换为方法调用来获取值(我们甚至可以制作方法internal 并使用InternalsVisibleTo 属性来制作方法对我们的测试程序集可见):

    [assembly: InternalsVisibleToAttribute("TestAssembly")]
    public sealed class DomainEntity
    {
        private string _name;
    
        public DomainEntity(string name) { _name = name; }
    
        public string Name => GetName();
    
        internal string GetName()
        {
            return _name;
        }
    }
    

    这里的缺点是……嗯,更多的代码、方法、更多的复杂性(而且为什么要这样编码还不是很明显)。

我的问题是:有没有“首选”的方式来做到这一点?为什么首选?

编辑:

  1. 我不想在测试中简单地使用类的实例的原因是构造函数中可能存在其他逻辑。如果我破坏了构造函数,它将破坏所有相关的测试(但它应该只破坏测试 DomainEntity 的测试)。

  2. 我可以提取一个接口并完成它。但我更喜欢使用接口来定义行为。这些课程没有。

【问题讨论】:

  • 你还没有解释为什么你不只是创建一个接口,或者为什么你首先需要模拟这个类?在您的示例中,您可以只使用 DomainEntity 的实例进行测试。
  • 据我所知,密封类无法模拟,因此将属性设为虚拟不是问题...您确定可以提取接口吗?
  • @stuartd 该示例已简化。 DomainEntity 的构造函数中可能存在逻辑。如果我破坏了构造函数,它将破坏所有需要 DomainEntity 的测试,这是不可取的。从理论上讲,我可以提取接口,但我认为它不合适。我更喜欢坚持“接口描述行为”,而这些类并没有真正的行为。
  • 这正是我们有接口的原因。没有接口就等于没有可测试性。
  • @PhilP.,如果您破坏 DomainEntity 的构造函数,依赖于 DomainEntity 的行为是否仍能正常工作?如果我破坏了某些功能,我希望所有依赖于该功能的测试都会失败。

标签: c# unit-testing abstract-class nsubstitute


【解决方案1】:

我可以提取一个接口并完成它。但我更喜欢使用接口来定义行为。而这些类都没有。

在这里避免使用接口可能会让自己的生活变得不必要地困难。如果您真的想“模拟”域实体(意思是用特定于测试的行为替换生产行为),我认为接口是要走的路。但是,您明确表示这些类没有行为,因此请继续阅读...

我不想在测试中简单地使用类的实例的原因是构造函数中可能存在额外的逻辑。如果我破坏了构造函数,它将破坏所有依赖测试(但它应该只破坏测试 DomainEntity 的测试)。

听起来你真的不需要模拟(正如我在上面定义的那样)——你只需要一种可维护的方式来实例化测试实例。

要解决这个问题,您可以引入builder 来构造DomainEntity 的实例。构建器将充当测试和实体构造函数之间的缓冲区或抽象。它可以为特定测试不关心的任何构造函数参数提供合理的默认值。

使用您在问题中定义的类作为起点,假设您有这样的测试(使用xUnit 语法):

[Fact]
public void Test1() {
    var entity = new DomainEntity("Valid");

    var nameChecker = new EntityNameChecker();

    Assert.True(nameChecker.IsDomainEntityNameValid(entity));
}

现在,也许我们想为域实体添加一个新的必需属性:

public sealed class DomainEntity {
    public string Name { get; private set; }
    public DateTimeOffset Date { get; private set; }

    public DomainEntity(string name, DateTimeOffset date) {
        Name = name;
        Date = date;
    }   
}

新的构造函数参数破坏了测试(可能还有很多其他测试)。

所以我们引入一个构建器:

public sealed class DomainEntityBuilder {
    public string Name { get; set; } = "Default Name";
    public DateTimeOffset Date { get; set; } = DateTimeOffset.Now;

    public DomainEntity Build() => new DomainEntity(Name, Date);
}

并稍微修改我们的测试:

[Fact]
public void Test1()
{
    // Instead of calling EntityBuilder's constructor, use DomainEntityBuilder
    var entity = new DomainEntityBuilder{ Name = "Valid" }.Build();

    var nameChecker = new EntityNameChecker();

    Assert.True(nameChecker.IsDomainEntityNameValid(entity));
}

测试不再与实体的构造函数紧密耦合。构建器为所有属性提供了合理的默认值,每个测试只提供与该特定测试相关的值。作为奖励,可以将方法(或扩展方法)添加到构建器中以帮助设置复杂的场景。

有些库可以帮助解决这类问题。我在几个不同的项目中使用过Bogus。我认为AutoFixture 是一个流行的选项,但我自己没有使用过。一个简单的构建器很容易实现,所以我建议从自制实现开始,只有在自制实现变得过于繁琐或难以维护时才添加第 3 方库。因为构建器本身就是一个抽象,所以如果/当时机成熟,很容易将其实现替换为基于库的实现。

【讨论】:

    【解决方案2】:

    我必须为我的测试模拟我的 DomainEntity。

    为什么?
    这就是您的方法朝着“困难”方向发展的地方。

    DomainEntity 有一个接受名称的构造函数,因此您可以使用它来设置测试实例。

    [Theory]
    [InlineData("Valid", true)]
    [InlineData("Not valid", false)]
    public void ShouldValidateName(string name, bool expected) 
    {
        var entity = new DomainEntity(name);
    
        var isValid = new EntityNameChecker().IsDomainEntityNameValid(entity);
    
        isValid.Should().Be(expected); // Pass
    }
    

    仅模拟使测试变慢或设置非常非常非常复杂的依赖项。
    例如,慢速测试通常是涉及外部资源(网络服务、数据库、文件系统等)的测试。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-04-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-21
      • 1970-01-01
      相关资源
      最近更新 更多