测试一个接口的多个实现(TDD)

Testing many implementations of an interface (TDD)

创建接口的多个实现以确保符合接口约定的最佳做法是什么?

public interface IFoo {
    /// <exception cref="System.ArgumentNullException">
    /// If <paramref name="baz"> is <c>null</c>.
    /// </exception>
    void Bar(Baz baz);
}

我在想一些类似的事情:

public abstract class IFooTestsBase {
    protected IFoo Foo;

    [Test]
    [ExpectedException(typeof(ArgumentNullException))]
    public void Bar_ThrowsException_WhenBazArgumentIsNull() {
        Foo.Bar(null);
    }
}

[TestFixture]
public class SpecialFooTests : IFooTestsBase {
    [TestFixtureSetUp]
    public void Init() {
        // Provide instance of `SpecialFoo` for inherited tests.
        Foo = new SpecialFoo();
    }

    // TDD as normal from here...
}

这是一种有效的 TDD 方法吗?

  1. 测试最初不会编译,因为 SpecialFoo class 没有被定义。

  2. 创建可编译但未通过所有测试的最小 SpecialFoo 实现。

  3. 然后测试将编译但不会通过,因为最小 SpecialFoo class 将不满足接口的期望。

或者我应该为 IFoo 的每个新实现逐个重新实现每个测试吗?

这是一种非常有效的方法,可以确保当前(和未来)的实现者遵守接口的(语义)契约。我第一次听说这些类型的测试是在 J.B。 Rainsberger,他们称他们为 contract tests.

如果您主要依赖孤立测试(即大量使用模拟),合同测试是编写集成测试(将多个真实对象粘合在一起)的必要替代方法。隔离单元测试验证一个单元是否在隔离状态下正常工作,而契约测试验证您对协作者所做的所有假设是否由他们的实现来保证。有关这种隔离测试风格的更多信息,您可以随时在 J.B 的博客上阅读更多内容。开始的好地方是 integrated tests are a scam.

J.B。是我认识的最顽固的测试驱动开发者之一。他使用的 TDD 风格专注于由外而内的 TDD,仅使用孤立的单元测试(即大量使用模拟)。在他的方法中,您测试驱动一个 SUT 并为您在 SUT 的隔离单元测试中模拟的每个协作者创建接口。设计 SUT 时,您会更深入一层并测试驱动合作者的实施。为了确保这些对象在粘合在一起时一起工作,他还建议编写这些合同测试,而不是编写将实际对象连接在一起的集成测试:对象为协作者所做的每个假设(例如:"all objects implementing this interface return null if they can't find a person with this id") 可以在合同测试中具体化。