如何用NUnit和C#测试含参数化构造函数的抽象类(含protected成员)
测试带有参数化构造函数的抽象类并访问protected成员的最优方案
问题描述
我需要测试一个带有参数化构造函数的抽象类,并且要访问其中标记为protected的成员。目前我构思了两种实现方式,先给出示例抽象类:
public abstract class MyAbstractClass { public MyAbstractClass(string someParam) { ... } protected int MethodToTest(int param1, int param2) { ... } }
已尝试的方案1:测试类直接继承抽象类
让TestFixture直接继承抽象类来访问protected成员,但需要在特性中传入构造参数,存在魔法字符串问题(虽可存入常量):
[TestFixture("constructor param here")] public class MyTestFixture : MyAbstractClass { public MyTestFixture(string someParam) : base(someParam) { ... } [Test] public void ConstructorTest() { Assert.Throws<ArgumentNullException>(() => _ = new MyTestFixture(null!)); } [Test] public void MethodTest() { Assert.That(this.MethodToTest(1, 2), Is.EqualTo(3)); } }
已尝试的方案2:创建包装类暴露protected成员
在测试项目中创建抽象类的私有包装类,将protected成员转为public,但类规模增大时会导致测试文件膨胀:
[TestFixture] public class MyTestFixture { private class MyAbstractClassWrapper : MyAbstractClass { public MyAbstractClassWrapper(string someParam) : base(someParam) { } public new int MethodToTest(int param1, int param2) => base.MethodToTest(param1, param2); } [Test] public void ConstructorTest() { Assert.Throws<ArgumentNullException>(() => _ = new MyAbstractClassWrapper(null!)); } [Test] public void MethodTest() { MyAbstractClassWrapper sut = new("constructor param"); Assert.That(sut.MethodToTest(1, 2), Is.EqualTo(3)); } }
请问是否有更优的解决方案,还是只能采用上述两种方式?
解决方案推荐
1. 使用模拟库(如Moq)动态生成子类
模拟库可以自动为抽象类生成动态子类,无需手动编写包装或继承,同时支持调用protected成员的真实实现:
[TestFixture] public class MyAbstractClassTests { [Test] public void Constructor_WithNullParam_ThrowsArgumentNullException() { Assert.Throws<ArgumentNullException>(() => new Mock<MyAbstractClass>(null!).Object); } [Test] public void MethodToTest_WithValidInputs_ReturnsExpectedValue() { // 创建模拟实例,传入构造参数,并指定调用基类真实实现 var mock = new Mock<MyAbstractClass>("test-param") { CallBase = true }; // 调用protected方法(Moq通过Protected()扩展支持) var result = mock.Object.MethodToTest(1, 2); Assert.That(result, Is.EqualTo(3)); } }
优点:无需额外代码,自动处理子类生成,支持灵活控制依赖;缺点:需要引入第三方库(Moq等),但这是.NET单元测试的常用工具。
2. 反射直接访问protected成员
无需继承或包装,通过反射创建抽象类实例并调用protected方法:
[TestFixture] public class MyAbstractClassTests { [Test] public void Constructor_NullParam_ThrowsException() { var exception = Assert.Throws<TargetInvocationException>(() => Activator.CreateInstance(typeof(MyAbstractClass), null!)); Assert.That(exception.InnerException, Is.TypeOf<ArgumentNullException>()); } [Test] public void MethodToTest_ValidParams_ReturnsCorrectValue() { // 创建抽象类的动态实例 var sut = Activator.CreateInstance(typeof(MyAbstractClass), "test-param") as MyAbstractClass; // 获取protected方法 var method = typeof(MyAbstractClass).GetMethod( nameof(MyAbstractClass.MethodToTest), BindingFlags.Instance | BindingFlags.NonPublic); // 调用方法并断言结果 var result = (int)method.Invoke(sut, new object[] { 1, 2 }); Assert.That(result, Is.EqualTo(3)); } }
优点:无额外依赖,无需修改原类;缺点:反射代码可读性较差,重构时方法名/参数变化不会触发编译错误,需注意维护。
3. 调整访问修饰符为internal(配合InternalsVisibleTo)
如果项目设计允许,可以将protected成员改为internal,然后在主项目的AssemblyInfo.cs中添加测试项目的可见性:
// 主项目AssemblyInfo.cs [assembly: InternalsVisibleTo("YourTestProjectName")]
测试代码可以直接创建实例并访问internal成员:
[TestFixture] public class MyAbstractClassTests { [Test] public void Constructor_NullParam_ThrowsException() { Assert.Throws<ArgumentNullException>(() => Activator.CreateInstance(typeof(MyAbstractClass), null!)); } [Test] public void MethodToTest_ValidParams_ReturnsCorrectValue() { var sut = Activator.CreateInstance(typeof(MyAbstractClass), "test-param") as MyAbstractClass; Assert.That(sut.MethodToTest(1, 2), Is.EqualTo(3)); } }
优点:代码简洁,无反射或包装开销;缺点:修改了原类的访问修饰符,可能不符合封装设计初衷,需权衡。
4. 改进包装类:提取通用包装逻辑
如果坚持使用包装类,可以把通用逻辑抽象成泛型基类,避免重复编写每个protected成员的包装:
// 通用抽象类包装基类 public abstract class AbstractWrapper<T> where T : class { protected T Instance { get; } protected AbstractWrapper(params object[] constructorArgs) { // 动态生成抽象类的子类实例 var type = typeof(T); var assembly = Assembly.GetAssembly(type); var dynamicType = assembly.DefinedTypes .FirstOrDefault(t => t.IsSubclassOf(type) && !t.IsAbstract); // 如果没有现成子类,用反射Emit生成(简化示例,实际可完善) if (dynamicType == null) { var name = $"Dynamic{type.Name}"; var tb = assembly.DefineDynamicModule(name).DefineType(name, TypeAttributes.Public, type); dynamicType = tb.CreateType(); } Instance = Activator.CreateInstance(dynamicType, constructorArgs) as T; } } // 针对MyAbstractClass的具体包装 private class MyAbstractClassWrapper : AbstractWrapper<MyAbstractClass> { public MyAbstractClassWrapper(string someParam) : base(someParam) { } // 仅需包装需要测试的protected成员 public int MethodToTest(int param1, int param2) => Instance.MethodToTest(param1, param2); }
优点:减少重复代码,包装类只需要关注要测试的成员;缺点:仍需维护包装类,但相比原始方案开销更小。
总结
- 优先推荐模拟库(Moq):无需额外代码,灵活可控,是.NET单元测试的标准方案;
- 若不想引入第三方库,反射是轻量选择,但需注意重构风险;
- 若项目允许调整访问权限,InternalsVisibleTo+internal是最简洁的方案;
- 你原有的两种方案也完全可行,若抽象类成员不多,包装类方案的维护成本其实很低。
内容的提问来源于stack exchange,提问作者mju
相关产品推荐
相关产品推荐

