在单元测试中模拟域实体 class' 属性:方法、抽象 classes 或虚拟属性?
Mocking domain entity class' properties in unit tests: methods, abstract classes or virtual properties?
我有一个代表域实体的 class,这个 class 不 实现任何接口。让我们考虑一些简单的事情:
public class DomainEntity
{
public DomainEntity(string name) { Name = name; }
public string Name { get; private set; }
}
我还有其他一些 class 正在测试。它有一个接受我的 DomainEntity
作为参数的方法,这个方法访问 Name
属性。例如:
public class EntityNameChecker : IEntityNameChecker
{
public bool IsDomainEntityNameValid(DomainEntity entity)
{
if (entity.Name == "Valid") { return true; }
return false;
}
}
我必须为我的测试模拟我的 DomainEntity
。我正在使用 NSubstitute
作为我的模拟库(它不允许模拟 non-virtual/non-abstract 属性)。
因此,在不添加接口 (a-la IDomainEntity
) 的情况下,我有三个选项来模拟剩余的 属性 值。
使 Name
属性 虚拟:
public class DomainEntity
{
public DomainEntity(string name) { Name = name; }
public virtual string Name { get; }
}
这里的缺点是我不能再让我的DomainEntity
classsealed
,这意味着任何消费者都可以继承它并覆盖我的Name
属性.
创建抽象"base" class并使用基础class类型作为参数类型:
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;
}
}
这里的缺点是过于复杂。这基本上将抽象 class 变成了某种接口。
不是直接访问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;
}
}
这里的缺点是......嗯,更多的代码、方法、更复杂的东西(而且为什么这样编码并不是很明显)。
我的问题是:是否有 "preferred" 方法可以做到这一点?为什么它是首选?
编辑:
我不想在我的测试中简单地使用 class 的实例的原因是构造函数中可能有额外的逻辑。如果我破坏构造函数,它将破坏所有相关测试(但它应该只破坏测试 DomainEntity 的测试)。
我可以提取一个接口并完成它。但我更喜欢使用接口来定义行为。而这些 class 有 none。
I could extract an interface and be done with it. But I prefer to use interfaces to define behaviors. And these classes have none.
如果避免使用此处的接口,您可能会给自己带来不必要的困难。如果你真的想要 "mock" 域实体(意思是,用特定于测试的行为替换生产行为),我认为接口是可行的方法。但是,您明确表示这些 类 没有任何行为,因此请继续阅读...
The reason I don’t want to simply use the instance of the class in my tests is the fact that there might be additional logic in the constructor. If I break the constructor it’s going to break all dependent tests (but it should break only the tests testing 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 in a few different projects. I think AutoFixture 是一个流行的选项,但我自己没有用过。一个简单的构建器很容易实现,所以我建议从自制实现开始,只有当自制实现变得过于乏味或复杂而无法维护时才添加第 3 方库。因为构建器本身就是一个抽象,所以很容易用基于库的实现替换它的实现 if/when 时机成熟。
I have to mock my DomainEntity for my test.
为什么?
这个,你的方法在 "difficult" 方向上消失了。
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
}
仅模拟使测试变慢或设置起来非常非常复杂的依赖项。
例如,慢速测试通常是涉及外部资源(网络服务、数据库、文件系统等)的测试。
我有一个代表域实体的 class,这个 class 不 实现任何接口。让我们考虑一些简单的事情:
public class DomainEntity
{
public DomainEntity(string name) { Name = name; }
public string Name { get; private set; }
}
我还有其他一些 class 正在测试。它有一个接受我的 DomainEntity
作为参数的方法,这个方法访问 Name
属性。例如:
public class EntityNameChecker : IEntityNameChecker
{
public bool IsDomainEntityNameValid(DomainEntity entity)
{
if (entity.Name == "Valid") { return true; }
return false;
}
}
我必须为我的测试模拟我的 DomainEntity
。我正在使用 NSubstitute
作为我的模拟库(它不允许模拟 non-virtual/non-abstract 属性)。
因此,在不添加接口 (a-la IDomainEntity
) 的情况下,我有三个选项来模拟剩余的 属性 值。
使
Name
属性 虚拟:public class DomainEntity { public DomainEntity(string name) { Name = name; } public virtual string Name { get; } }
这里的缺点是我不能再让我的
DomainEntity
classsealed
,这意味着任何消费者都可以继承它并覆盖我的Name
属性.创建抽象"base" class并使用基础class类型作为参数类型:
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; } }
这里的缺点是过于复杂。这基本上将抽象 class 变成了某种接口。
不是直接访问
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; } }
这里的缺点是......嗯,更多的代码、方法、更复杂的东西(而且为什么这样编码并不是很明显)。
我的问题是:是否有 "preferred" 方法可以做到这一点?为什么它是首选?
编辑:
我不想在我的测试中简单地使用 class 的实例的原因是构造函数中可能有额外的逻辑。如果我破坏构造函数,它将破坏所有相关测试(但它应该只破坏测试 DomainEntity 的测试)。
我可以提取一个接口并完成它。但我更喜欢使用接口来定义行为。而这些 class 有 none。
I could extract an interface and be done with it. But I prefer to use interfaces to define behaviors. And these classes have none.
如果避免使用此处的接口,您可能会给自己带来不必要的困难。如果你真的想要 "mock" 域实体(意思是,用特定于测试的行为替换生产行为),我认为接口是可行的方法。但是,您明确表示这些 类 没有任何行为,因此请继续阅读...
The reason I don’t want to simply use the instance of the class in my tests is the fact that there might be additional logic in the constructor. If I break the constructor it’s going to break all dependent tests (but it should break only the tests testing 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 in a few different projects. I think AutoFixture 是一个流行的选项,但我自己没有用过。一个简单的构建器很容易实现,所以我建议从自制实现开始,只有当自制实现变得过于乏味或复杂而无法维护时才添加第 3 方库。因为构建器本身就是一个抽象,所以很容易用基于库的实现替换它的实现 if/when 时机成熟。
I have to mock my DomainEntity for my test.
为什么?
这个,你的方法在 "difficult" 方向上消失了。
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
}
仅模拟使测试变慢或设置起来非常非常复杂的依赖项。
例如,慢速测试通常是涉及外部资源(网络服务、数据库、文件系统等)的测试。