32

如果我有接口 IFoo,并且有几个实现它的类,那么针对接口测试所有这些类的最佳/最优雅/最聪明的方法是什么?

我想减少测试代码的重复,但仍然“忠于”单元测试的原则。

您认为最佳做法是什么?我正在使用 NUnit,但我认为任何单元测试框架中的示例都是有效的

4

6 回答 6

15

如果您的类实现了任何一个接口,那么它们都需要实现该接口中的方法。为了测试这些类,您需要为每个类创建一个单元测试类。

让我们改用更智能的路线;如果您的目标是避免代码和测试代码重复,您可能希望创建一个抽象类来处理重复出现的代码。

例如,您有以下界面:

public interface IFoo {

    public void CommonCode();

    public void SpecificCode();

}

您可能想要创建一个抽象类:

public abstract class AbstractFoo : IFoo {

    public void CommonCode() {
          SpecificCode();
    }

    public abstract void SpecificCode();

}

测试很容易;将测试类中的抽象类实现为内部类:

[TestFixture]
public void TestClass {

    private class TestFoo : AbstractFoo {
        boolean hasCalledSpecificCode = false;
        public void SpecificCode() {
            hasCalledSpecificCode = true;
        }
    }

    [Test]
    public void testCommonCallsSpecificCode() {
        TestFoo fooFighter = new TestFoo();
        fooFighter.CommonCode();
        Assert.That(fooFighter.hasCalledSpecificCode, Is.True());
    }
}

...或者让测试类扩展抽象类本身,如果这符合您的喜好。

[TestFixture]
public void TestClass : AbstractFoo {

    boolean hasCalledSpecificCode;
    public void specificCode() {
        hasCalledSpecificCode = true;
    }

    [Test]
    public void testCommonCallsSpecificCode() {
        AbstractFoo fooFighter = this;
        hasCalledSpecificCode = false;
        fooFighter.CommonCode();
        Assert.That(fooFighter.hasCalledSpecificCode, Is.True());
    }        

}

拥有一个抽象类来处理接口所暗示的公共代码可以提供更简洁的代码设计。

我希望这对你有意义。


作为旁注,这是一种称为模板方法模式的常见设计模式。在上面的例子中,模板方法就是CommonCode方法,SpecificCode被称为存根或钩子。这个想法是任何人都可以扩展行为而无需了解幕后的东西。

很多框架都依赖于这种行为模式,例如ASP.NET,您必须在页面或用户控件中实现挂钩,例如Page_LoadLoad事件调用的生成方法,模板方法在幕后调用挂钩。这方面的例子还有很多。基本上,您必须使用“load”、“init”或“render”等词来实现的任何内容都由模板方法调用。

于 2008-09-02T08:39:09.847 回答
11

我不同意Jon Limjap的说法,他说,

这不是关于 a.) 如何实现该方法和 b.) 该方法应该做什么的合同(它只保证返回类型),我收集的两个原因将是您想要这种方法的动机的测试。

合同的许多部分可能没有在返回类型中指定。一个与语言无关的例子:

public interface List {

  // adds o and returns the list
  public List add(Object o);

  // removed the first occurrence of o and returns the list
  public List remove(Object o);

}

您对 LinkedList、ArrayList、CircularlyLinkedList 和所有其他人的单元测试不仅应该测试返回的列表本身,还应该测试它们是否已被正确修改。

之前有一个关于按合同设计的问题,它可以帮助您在干燥这些测试的一种方式上指明正确的方向。

如果您不想要合同的开销,我建议按照Spoike的建议进行测试台:

abstract class BaseListTest {

  abstract public List newListInstance();

  public void testAddToList() {
    // do some adding tests
  }

  public void testRemoveFromList() {
    // do some removing tests
  }

}

class ArrayListTest < BaseListTest {
  List newListInstance() { new ArrayList(); }

  public void arrayListSpecificTest1() {
    // test something about ArrayLists beyond the List requirements
  }
}
于 2008-09-02T12:16:24.957 回答
3

我认为这不是最佳做法。

一个简单的事实是,接口只不过是实现方法的合同。它不是关于 a.) 如何实现该方法和 b.) 该方法应该做什么的合同(它只保证返回类型),我收集的两个原因将是您想要这种方法的动机的测试。

如果您真的想控制您的方法实现,您可以选择:

  • 将其实现为抽象类中的方法,并从中继承。你仍然需要将它继承到一个具体的类中,但你确信除非它被显式覆盖,否则该方法将做正确的事情。
  • 在 .NET 3.5/C# 3.0 中,将该方法实现为引用接口的扩展方法

例子:

public static ReturnType MethodName (this IMyinterface myImplementation, SomeObject someParameter)
{
    //method body goes here
}

任何正确引用该扩展方法的实现都将精确地发出该扩展方法,因此您只需要对其进行一次测试。

于 2008-09-02T08:41:32.663 回答
1

在测试接口或基类契约时,我更喜欢让测试框架自动负责查找所有实现者。这使您可以专注于被测接口,并合理地确定所有实现都将被测试,而无需进行大量手动实现。

  • 对于xUnit.net,我创建了一个类型解析器库来搜索特定类型的所有实现(xUnit.net 扩展只是对类型解析器功能的一个薄包装,因此它可以适应在其他框架中使用)。
  • MbUnit中,您可以在参数上使用CombinatorialTestwithUsingImplementations属性。
  • 对于其他框架,提到的基类模式Spoike可能很有用。

除了测试接口的基础知识之外,您还应该测试每个单独的实现是否遵循其特定要求。

于 2008-09-03T00:24:23.490 回答
1

[TestFixture] 类的层次结构如何?将通用测试代码放在基测试类中,继承到子测试类中。

于 2008-09-14T19:01:34.053 回答
0

我不使用 NUnit,但我测试了 C++ 接口。我将首先测试一个 TestFoo 类,它是它的基本实现,以确保通用的东西有效。然后你只需要测试每个接口独有的东西。

于 2008-09-02T08:16:43.207 回答