如何在单元测试中设置自增ID?实体私有Setter的测试解决方法
我有一个自增ID的实体类,为了保持领域模型的封装性,把Id的Setter设为private。但在单元测试时,需要手动设置或获取这个ID来验证逻辑,比如在测试WorkingGroup关联WorkingTime的场景时,需要指定WorkingGroupId的值。
我的实体类代码:
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));
现在我想在测试中设置WorkingGroup的ID,同时不想把Id的Setter改成public,应该怎么做?
这是个很常见的领域模型测试痛点——既要守住封装性(不让外部随意修改自增ID),又要在测试场景中灵活控制ID值来验证关联逻辑。下面是几种不用把Id的Setter改成public的可行方案:
解决方案1:使用反射设置私有属性
反射是最直接的方式,不需要修改实体类代码,适合快速测试场景。你可以通过反射获取私有属性的Setter方法,然后设置值:
// 假设WorkingGroup类也有private set的Id属性 var workingGroup = new WorkingGroup("WG", 3, Week.Sunday, 2); // 获取Id属性的Setter var idProperty = typeof(WorkingGroup).GetProperty("Id", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); var setter = idProperty.GetSetMethod(nonPublic: true); // 设置ID值 setter.Invoke(workingGroup, new object[] { 1 }); // 之后就可以用这个ID创建WorkingTime了 workingGroup.AssignedWorkingTimes.Add(new WorkingTime("Winter", 8, 1, workingGroup.Id));
如果觉得每次写反射麻烦,可以封装一个通用的工具方法:
public static void SetPrivateProperty<T>(T obj, string propertyName, object value) { var property = typeof(T).GetProperty(propertyName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); var setter = property.GetSetMethod(nonPublic: true); setter.Invoke(obj, new object[] { value }); } // 使用时 SetPrivateProperty(workingGroup, "Id", 1);
解决方案2:利用InternalsVisibleTo特性,将Setter设为internal
如果你不想用反射,可以把Id的Setter改成internal,然后在项目的AssemblyInfo.cs中添加InternalsVisibleTo特性,让测试程序集可以访问内部成员:
首先修改实体类:
public int Id { get; internal set; }
然后在领域模型项目的AssemblyInfo.cs中添加:
[assembly: InternalsVisibleTo("YourTestProjectName")]
这样在测试项目中就可以直接设置Id了:
var workingGroup = new WorkingGroup("WG", 3, Week.Sunday, 2); workingGroup.Id = 1; // 测试项目中可以直接访问
这种方式比反射更优雅,同时也不会把Setter暴露给其他外部项目,只对测试程序集开放。
解决方案3:添加测试专用的构造函数或工厂方法
你可以为实体类添加一个仅用于测试的构造函数(标记为internal或用[Obsolete]提示不要在生产代码中使用),或者创建一个测试专用的工厂类:
方法A:测试专用构造函数
// 生产环境用的构造函数 public WorkingGroup(string name, int someParam, Week week, int anotherParam) { // 初始化逻辑 } // 测试专用构造函数,internal修饰 internal WorkingGroup(int id, string name, int someParam, Week week, int anotherParam) { Id = id; Name = name; // 其他初始化逻辑 }
然后配合InternalsVisibleTo,测试中就可以直接用这个构造函数创建带指定ID的实例:
var workingGroup = new WorkingGroup(1, "WG", 3, Week.Sunday, 2);
方法B:测试工厂类
创建一个测试项目中的工厂类,负责创建带指定ID的实体:
public static class WorkingGroupTestFactory { public static WorkingGroup CreateWithId(int id, string name, int someParam, Week week, int anotherParam) { var workingGroup = new WorkingGroup(name, someParam, week, anotherParam); // 这里用反射设置ID,封装起来 SetPrivateProperty(workingGroup, "Id", id); return workingGroup; } private static void SetPrivateProperty<T>(T obj, string propertyName, object value) { var property = typeof(T).GetProperty(propertyName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); var setter = property.GetSetMethod(nonPublic: true); setter.Invoke(obj, new object[] { value }); } }
测试时直接调用:
var workingGroup = WorkingGroupTestFactory.CreateWithId(1, "WG", 3, Week.Sunday, 2);
解决方案4:使用Mock框架(如Moq)
如果你用Moq这样的Mock框架,可以直接Mock实体类(需要实体类是virtual或者接口),然后设置属性值:
var mockWorkingGroup = new Mock<WorkingGroup>("WG", 3, Week.Sunday, 2); mockWorkingGroup.SetupProperty(g => g.Id, 1); // 设置Id属性 var workingGroup = mockWorkingGroup.Object; workingGroup.AssignedWorkingTimes.Add(new WorkingTime("Winter", 8, 1, workingGroup.Id));
注意:这种方式要求Id属性是virtual,或者你针对接口进行Mock。
要不要改成public Setter?
不建议直接把Id的Setter改成public,因为这会破坏领域模型的封装性——自增ID应该由数据库或领域服务来分配,而不是随便外部代码就能修改。上面的几种方法都能在不破坏封装的前提下满足测试需求,优先推荐解决方案2(InternalsVisibleTo+internal Setter)或者解决方案3(测试专用构造函数/工厂),这两种方式更清晰且易于维护。
内容的提问来源于stack exchange,提问作者Anyname Donotcare

