C# 单元测试验证 Activator 实例化的 class 上的非抽象方法

C# Unit tests verify on non-abstract method on class instantied by Activator

首先介绍一下我的项目。

我们正在开发一个应用程序,用户可以在其中使用程序。作为程序,我指的是机密使用说明列表。

有不同类型的程序都继承自程序抽象基 class。

由于用户可以创建不同类型的程序,我们开发了一个 ProgramManager 可以根据其类型实例化任何类型的程序。我们不需要实例化抽象 class 但所有具体的 classes(并且它有效)但是因为具体的 Program 具有相同的方法(AddNewChannel,Save,...)我们像 Programs 一样处理它们.

这是一个代码示例:

public Program CreateProgram(Type type)
    {
        Program program = Activator.CreateInstance(type) as Program;
        program.Directory = ProgramsPath;

        int nbChannels = 2; //not 2 but a long line searching the correct number where it is.

        for (int i = 1; i <= nbChannels; i++)
        {
            program.AddNewChannel(i);
        }

        program.Save();
        return program;
    }

我现在要做的是测试这个功能,我不想重复我已经为不同程序所做的单元测试 classes。

例如,这是我的一个测试函数(用于 Save 方法),它是 init。我将需要测试的类型存储在 xml 文件中。

    [TestInitialize]
    public void TestInitialize()
    {
        if (!TestContext.TestName.StartsWith("GetKnownTypes"))
            type = UnitTestsInitialization.applicationHMIAssembly.GetType((string)TestContext.DataRow["Data"]);
    }


    [TestMethod]
    [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML",
               "|DataDirectory|\" + DATA_FILE, "Row",
                DataAccessMethod.Sequential)]
    public void SavedProgramCreatesFile()
    {
        Program program = Activator.CreateInstance(type) as Program;
        program.Name = "SavedProgramCreatesFile";
        program.Directory = DIRECTORY;

        program.Save();

        string savedProgramFileName = program.GetFilePath();

        bool result = File.Exists(savedProgramFileName);

        Assert.IsTrue(result);
    }

我所有的具体程序 classes 已经单独测试。

因此,我想测试以下方法program.AddNewChannelprogram.Save是否被调用。

我查看了 Moq,但第一个问题是方法 Save 不是抽象的。

此外,使用 Activator 不允许我制作 Mock<Program>

我在单元测试中尝试了以下操作,以尝试实例化模拟并像程序一样使用它:

    [TestMethod]
    [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML",
               "|DataDirectory|\" + DATA_FILE, "Row",
                DataAccessMethod.Sequential)]
    public void CreateProgram_CallsProgramSaveMethod()
    {
        Mock<Program> mock = new Mock<Program>();
        mock.Setup(p => p.AddNewChannel(It.IsAny<int>()));

        Program program = pm.CreateProgram(mock.Object.GetType());
        mock.Verify(p => p.Save());
        mock.Verify(p => p.GetFilePath(It.IsAny<string>()));
        mock.Verify(p => p.AddNewChannel(It.IsAny<int>()), Times.Exactly(ProgramManager.NB_MACHINE_CHANNELS));
        Assert.IsNotNull(program);
        program.DeleteFile();
    }

这是受这个问题的启发:How to mock An Abstract Base Class

它会一直工作,直到到达 for 循环中的 program.AddNewChannel(i); 行。错误如下:

System.NotImplementedException: 'This is a DynamicProxy2 error: The interceptor attempted to 'Proceed' for method 'Void AddNewChannel(Int32)' which is abstract. When calling an abstract method there is no implementation to 'proceed' to and it is the responsibility of the interceptor to mimic the implementation (set return value, out arguments etc)'

似乎设置不起作用,但我可能明白为什么。 (我尝试实例化一个没有实现验证方法的 Proxy 子类型)

我还尝试在我的程序 class 上使用 Proxy,它将实现一个包含我需要的方法的接口,但这里的问题又是激活器。

任何人都可以建议我测试这些方法调用的方法吗? (即使我需要改变我的方法CreateProgram

我在这里看了看:How to mock non virtual methods? 但我不确定这是否适用于我的问题。

我使用 MSTests 进行单元测试。

通知

其他一切正常。我的所有其他测试都顺利通过,而且我的代码似乎可以正常工作(手动测试)。

提前致谢。

问题的根本原因是您使用类型作为参数,然后使用它来创建该类型的实例。但是,您传递的是抽象类型 class,它不是专门用于实例化的。 您需要直接使用具体的 classes


Thereby, I would like to test if the following methods program.AddNewChannel and program.Save are called.

作为测试,这还不够。您想要测试这些方法 是否按预期工作 ,而不仅仅是调用它们然后假设它们工作。

您描述的是(非常基本的)集成测试,而不是单元测试。


I don't want to duplicate the unitTests I already made for the different Program classes

这是一个非常危险的决定。单元测试背后的部分想法是为不同的(具体)对象创建 单独的 测试。测试需要尽可能合理地隔离。您正在尝试重用测试逻辑,这是一件好事,但需要以不影响测试隔离的方式进行。

但是有一些方法可以在不影响测试隔离的情况下做到这一点。 我只有 NUnit 的测试经验,但我认为类似的方法也适用于其他框架。

假设如下:

public abstract class Program 
{
    public bool BaseMethod() {}
}

public class Foo : Program 
{
    public bool CustomFooMethod() {}
}

public class Bar : Program 
{
    public bool CustomBarMethod() {}
}

创建抽象class测试方法:

[TestFixture]
[Ignore]
public class ProgramTests
{
     public virtual Program GetConcrete()
     {
         throw new NotImplementedException();
     }

    [Test]
    public void BaseMethodTestReturnsFalse()
    {
        var result = GetConcrete().BaseMethod();

        Assert.IsFalse(result);
    }
}

[Ignore] 确保 ProgramTests class 不会自行测试。

然后你从这个class继承,具体的classes将被测试:

[TestFixture]
public class FooTests
{
    private readonly Foo Foo;

    public FooTests()  
    {
        this.Foo = new Foo();
    }

    public overrides Program GetConcrete()
    {
        return this.Foo;
    }

    [Test]
    public void CustomFooMethodTestReturnsFalse()
    {
        var result = this.Foo.CustomFooMethod();

        Assert.IsFalse(result);
    }
}

BarTests类似实现。

NUnit(可能还有其他测试框架)将发现所有继承的测试,并将 运行 那些派生的测试 class。因此,从 ProgramTests 派生的每个 class 将始终包含 BaseMethodTestReturnsTrue 测试。

这样,您的基础 class' 测试可重复使用,但每个具体 class 仍将单独测试。这保持了您的测试分离,同时也避免了您必须 copy/paste 为每个具体 class.

测试逻辑

我也注意到了这一点:

Mock<Program> mock = new Mock<Program>();
mock.Setup(p => p.AddNewChannel(It.IsAny<int>()));
Program program = pm.CreateProgram(mock.Object.GetType());

我不明白这段代码的用途。它与简单地做有什么不同:

Program program = pm.CreateProgram(typeof(Program).GetType());

据我所知,模拟及其设置都是无关紧要的,因为您只查看其 类型 然后让 CreateProgram() 创建一个新的无论如何都反对你。

其次,这参考了我测试具体 classes 的例子,你不应该直接用 Program 测试,你应该测试你的派生程序 classes(FooBar)。

这是问题的根本原因。您使用类型作为参数,然后使用它来创建该类型的实例。但是,您传递的是抽象类型 class,它不是专门用于实例化的。 您需要直接使用具体的 classes

创建包装器接口并 class 围绕 Activator,然后将类型传递给它:

public interface IActivatorWrapper
{
    object CreateInstance(Type type);
}

public class ActivatorWrapper : IActivatorWrapper
{
    public object CreateInstance(Type type)
    {
        return Activator.CreateInstance(type);
    }
}

直接使用它而不是 Activator,然后将 IActivatorWrapper 模拟为 return 您想要的任何模拟对象。


另一个帮助您解决问题的想法是向您的抽象 Program class 添加一个 IProgram 接口,然后使用它来引用您的具体 Program实例。这也可能对您有所帮助,如果您想要编写具有不同基数 class 的具体 Program