You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 22:00:23