如何在单元测试中设置自动增量ID

时间:2018-05-16 13:31:47

标签: c# unit-testing domain-driven-design

我面临以下情况:

我的实体的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

  

我应该使用公共设置器吗?

3 个答案:

答案 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。