我面临以下情况:
我的实体的ID是自动递增的,所以我将它们的setter设置为private
,但是当我想对我的域类进行单元测试时,我发现自己需要获取并设置ID。
如何设置ID?
public WorkingTime(string name, short numberOfHours, short numberOfShortDays, int workingGroupId)
{
Name = name;
NumberOfHours = numberOfHours;
NumberOfShortDays = numberOfShortDays;
WorkingGroupId = workingGroupId;
ActivatedWorkingTimes = new List<WorkingTimeActivation>();
}
private ICollection<WorkingTimeActivation> _activatedWorkingTimes;
public int Id { get; private set; }
public string Name { get; set; }
public short NumberOfHours { get; set; }
public short NumberOfShortDays { get; set; }
public int WorkingGroupId { get; set; }
public virtual WorkingGroup WorkingGroup { get; set; }
public virtual ICollection<WorkingTimeActivation> ActivatedWorkingTimes { get => _activatedWorkingTimes; set => _activatedWorkingTimes = value; }
测试示例:
var workingGroup = new WorkingGroup("WG", 3, Week.Sunday, 2);
workingGroup.AssignedWorkingTimes.Add(new WorkingTime("Winter", 8, 1, 1));
workingGroup.AssignedWorkingTimes.Add(new WorkingTime("Summer", 6, 0, 1));
现在我要设置workingGroupId
我应该使用公共设置器吗?
答案 0 :(得分:3)
我应该使用公共设置器吗?
可能不是,不。
有两种选择需要考虑。
一个是,如果标识符确实是私有数据,则测试不需要直接与之交互。我们将测试的范围限制在模型中的 observable 副作用,以便我们可以自由地稍后更改私有实现细节。
另一个原因是该测试试图引起您的注意,即您可能会使用不同的策略来生成标识符,并且不同的策略可能适用于不同的上下文 - - 生产代码中使用的一种策略,另一种用于测试工具。
基本模式隔离了策略并提供了控制应用哪种策略所需的功能。在测试中,我们使用test double实施策略合同,这允许测试保持对可能是任意副作用的确定性控制。
答案 1 :(得分:1)
如果Id
需要保持对类库的私密性,并且还需要通过测试项目进行测试,在我看来,这是使用C#时的下一个最佳选择:
将private set;
更改为internal set;
或创建内部方法以设置ID。
public int Id { get; internal set; }
在具有需要测试的类的类库中,在解决方案资源管理器中,在“属性”下的AssemblyInfo.cs文件中,添加以下行:
[assembly: InternalsVisibleTo("MyTestingAssembly")]
这将使您需要测试的类库中的Internals仅对测试项目可见。这不会解决从类库中的其他代码封装Id
的问题,但它会使Id
的内部关注对其他未测试类库的DLL可见。
答案 2 :(得分:1)
您似乎希望通过创建私有setter来保持域图层中的实体“纯粹”。这是一件好事。我处理测试或将DTO映射到实体的一种方法是创建一个扩展方法,以便访问私有的setter。
此方法使用Marc Gravell的FastMember库:
using FastMember;
...
public static class DomainExtensions
{
private static readonly IDictionary<Type, TypeAccessor> _accessors = new Dictionary<Type, TypeAccessor>();
public static T With<T, TFieldOrProperty>(this T instance, Expression<Func<T, TFieldOrProperty>> fieldOrProperty, TFieldOrProperty value)
where T : class
{
if (instance == null)
return null;
if (!(fieldOrProperty.Body is MemberExpression member))
throw new ArgumentException($"Expression '{fieldOrProperty}' is not for a property or field.");
try
{
if (!_accessors.TryGetValue(typeof(T), out var ta))
lock (_accessors)
ta = _accessors[typeof(T)] = TypeAccessor.Create(typeof(T), true);
if (ta[instance, member.Member.Name] != null)
{
ta[instance, member.Member.Name] = value;
return instance;
}
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
}
// fallback to reflection
var fi = member.Member as FieldInfo;
fi?.SetValue(instance, value);
var pi = member.Member as PropertyInfo;
pi?.SetValue(instance, value);
return instance;
}
然后你就这样使用它:
workingGroup.With(wg => wg.WorkingGroupId, 2);
通过这种方式,您的实体保持纯洁。现在你可以说,“我现在可以绕过实体的只读性质。”这是真的,但是开发人员必须明确地并通过调用With()
来谨慎地违反此访问权。
如果测试用例是您需要此访问权限的唯一位置,请将With()
方法放入程序集中并进行测试。这可以保证您的生产代码无法设置ID。