【问题标题】:Can I implement a series of reusable tests to test an interface's implementation?我可以实现一系列可重用的测试来测试接口的实现吗?
【发布时间】:2012-03-11 04:19:00
【问题描述】:

我正在用 C# 编写一系列集合类,每个集合类都实现了类似的自定义接口。是否可以为一个接口编写一个单元测试集合,并在几个不同的实现上自动运行它们?我想避免每个实现的任何重复测试代码。

我愿意研究任何框架(NUnit 等)或 Visual Studio 扩展来完成此任务。


对于那些希望这样做的人,我发布了基于avandeursen's accepted solution 的具体解决方案an answer

【问题讨论】:

  • 添加了 lsp 作为标签,因为问题和答案适用于任何遵循 LSP 的类层次结构。

标签: .net visual-studio-2010 unit-testing liskov-substitution-principle


【解决方案1】:

这是我基于avandeursen's answer的具体实现:

[TestClass]
public abstract class IMyInterfaceTests
{
    protected abstract IMyInterface CreateInstance();

    [TestMethod]
    public void SomeTest()
    {
        IMyInterface instance = CreateInstance();
        // Run the test
    }
}

然后每个接口实现定义以下测试类:

[TestClass]
public class MyImplementationTests : IMyInterfaceTests
{
    protected override IMyInterface CreateInstance()
    {
        return new MyImplementation();
    }
}

SomeTest 对派生自IMyInterfaceTests 的每个具体TestClass 运行一次。通过使用抽象基类,我避免了任何模拟实现的需要。请务必将TestClassAttribute 添加到两个类,否则这将不起作用。最后,如果需要,您可以向子类添加任何特定于实现的测试(例如构造函数)。

【讨论】:

  • 您使用的是特定的测试框架吗?我在 VS2012 中使用包含的单元测试,并且从另一个“共享”测试项目(命名空间)继承的测试没有被拾取。
  • 奇怪——在同一个项目中继承的测试,不同的命名空间显示得很好。只有当他们在不同的项目中时才不会注册。我错过了什么?
【解决方案2】:

是的,这是可能的。诀窍是让您的单元类测试层次结构遵循代码的类层次结构。

假设您有一个接口Itf,其中实现类C1C2

您首先为Itf (ItfTest) 创建一个测试类。要实际进行测试,您需要创建 Itf 接口的模拟实现。

ItfTest 中的所有测试都应该通过Itf (!) 的任何实现。如果不是,您的实现不符合Liskov Substitution Principle(Martin 的SOLID OO 设计原则中的“L”)

因此,要为C1 创建一个测试用例,您的C1Test 类可以扩展ItfTest。您的扩展应该用 C1 对象的创建(将其注入或使用 GoF factory method)替换模拟对象的创建。通过这种方式,所有ItfTest 案例都应用于C1 类型的实例。此外,您的 C1Test 类可以包含特定于 C1 的其他测试用例。

C2 也是如此。并且您可以对更深的嵌套类和接口重复该技巧。

参考:Binder 的Polymorphic Server Test 模式, 和 McGregor 的 PACT -- 组件测试的并行架构。

【讨论】:

  • 为了避免模拟实现,我只是将我的 ItfTest 类声明为抽象类并声明了一个 protected abstract Itf CreateInstance(); 函数存根。 (注意ItfTestC1Test 都需要有[TestClass] 属性。)
  • 是的,我也使用了工厂方法,因为它更简单。我在 JUnit 中使用了这种测试方法,其中没有 [TestClass] 属性,但是继承了超类中的 @Test 注释,导致超类测试用例在子类中重新运行。并且:感谢您的编辑。
【解决方案3】:

您可以使用MBUnit 中的[RowTest] 属性来执行此操作。下面的例子展示了你在哪里向方法传递一个字符串来指示你想实例化哪个接口实现类,然后通过反射创建这个类:

[RowTest]
[Row("Class1")]
[Row("Class2")]
[Row("Class3")]
public void TestMethod(string type)
{
   IMyInterface foo = Activator.CreateInstance(Type.GetType(type)) as IMyInterface;

   //Do tests on foo:
}

在 [Row] 属性中,您可以传递任意数量的输入参数,例如用于测试的输入值或方法调用返回的预期值。您需要添加相应类型的参数作为测试方法输入参数。

【讨论】:

  • 您能否编辑您的答案以准确解释我将如何以这种方式测试实现?您似乎是在说每个单元测试都需要一个用于所有实现的 switch 语句,这不是我想要的。
  • 如果您不想使用 switch 语句,则无需使用。我提到你也可以使用反射。有关此示例,请参见 Anthony 的回答。我还更新了我的帖子以包含一个使用反射来实例化接口的各种具体实现的示例。
  • 我可能会写这个来获取 IMyInterface 的实例,而不仅仅是名称,但仍然是 +1。
  • 感谢丹的想法。我从来没有能够让 MbUnit 行测试属性与常量以外的任何东西一起使用。例如,这样的事情不会编译:[Row(new Class1())]。我认为这是 .net 属性参数的限制。我不知道是否可以使用其他测试框架来做这样的事情。
  • @JoeAlfano:.NET 属性参数实际上几乎只能是常量 (more)。
【解决方案4】:

扩展Joe's 答案,您可以在 NUnit 中以类似于 MBUnit 的 RowTest 的方式使用 [TestCaseSource] 属性。您可以在其中创建一个包含您的类名的测试用例源。然后,您可以装饰 TestCaseSource 属性的每个测试。然后使用 Activator.CreateInstance 你可以强制转换到界面并且你会被设置。

类似这样的东西(注意 - 头编译)

string[] MyClassNameList = { "Class1", "Class2" };

[TestCaseSource(MyClassNameList)]
public void Test1(string className)
{
    var instance = Activator.CreateInstance(Type.FromName(className)) as IMyInterface;

    ...
}

【讨论】:

    猜你喜欢
    • 2022-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-24
    • 1970-01-01
    相关资源
    最近更新 更多