C#单元测试对由Activator实例化的类的非抽象方法进行了验证

时间:2018-09-18 13:14:40

标签: c# unit-testing abstract activator

首先,让我向您介绍我的项目。

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

有不同类型的程序,都从程序抽象基类继承。

由于用户可以创建不同类型的程序,因此我们开发了一个ProgramManager,可以按其类型实例化任何类型的程序。我们不需要实例化抽象类,但是所有具体的类都可以实例化(并且可以工作),但是由于具体的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;
    }

我现在要做的是测试此功能,并且我不想重复为不同的Program类所做的unitTest。

作为示例,这是我的测试函数之一(用于Save方法)。我将需要测试的类型存储在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);
    }

我所有的具体Program类均已分别测试。

因此,我想测试是否调用了以下方法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:'这是一个DynamicProxy2错误:拦截器试图为抽象的方法'Void AddNewChannel(Int32)'进行'继续'操作。调用抽象方法时,没有实现要“进行”,并且拦截器有责任模仿实现(设置返回值,输出参数等)'

该设置似乎无法正常运行,但我可能会理解为什么。 (我尝试实例化未实现verify方法的Proxy子类型)

我还尝试在程序类上使用代理,该代理将实现一个接口,该接口将包含我需要的方法,但这里的问题仍然是激活器。

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

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

我将MSTests用于单元测试。

注意

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

谢谢。

2 个答案:

答案 0 :(得分:2)

此问题的根本原因是您将类型用作参数,然后使用该参数创建此类型的实例。但是,您传入的是抽象类的类型,该类不是专门为实例化的。 您需要直接使用具体的课程


  

因此,我想测试是否调用了以下方法program.AddNewChannel和program.Save。

这还不足以作为测试。您想测试这些方法是否有效,而不只是是否被调用,然后假设它们有效。

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


  

我不想重复为不同的程序类制作的unitTests

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

但是有一些方法可以做到这一点而又不会损害您的测试隔离度。 我只有NUnit的测试经验,但我认为在其他框架中也可以使用类似的方法。

假设以下内容:

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

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

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

创建抽象类测试方法:

[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类自身不会受到测试。

然后您从此类继承,该类将在其中进行测试:

[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(可能还有其他测试框架)将发现所有继承的测试,并为派生类运行这些测试。因此,从ProgramTests派生的每个类都将始终包含BaseMethodTestReturnsTrue测试。

这样,您的基类的测试可以重用,但是每个具体类仍将分别进行测试。这样既可以保持测试分离,又可以避免必须为每个具体类复制/粘贴测试逻辑。


我也注意到了这一点

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()都会为您创建一个新对象。

第二,这又回到了我测试具体类的示例,您不应该直接使用Program进行测试,而是应该测试派生的程序类(Foo和{{1 }})。

这是问题的根本原因。您将类型用作参数,然后使用该参数创建此类型的实例。但是,您传入的是抽象类的类型,该类不是专门为实例化的。 您需要直接使用具体的课程

答案 1 :(得分:1)

创建一个包装器接口并围绕Activator进行分类,然后将类型传递给该接口:

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

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

直接使用它代替Activator,然后对IActivatorWrapper进行模拟,以返回所需的任何模拟对象。


另一个解决问题的方法是在抽象IProgram类中添加一个Program接口,然后使用该接口引用具体的Program实例。如果您想使用其他基类编写具体的Program,这也可能对您有所帮助。