你是否应该提前知道每个测试的依赖关系

Should you know your dependencies in advance for each test

我正在探索 TDD 和 SOLID 原则。假设我有一个服务,我在编写实现之前为其创建了一个失败测试。

public interface IService {

    bool DoSomething();

}

public class Service : IService {

    bool DoSomething() {
    ...
    }

}

[TestMethod]
public void ServiceImplementationTest()
{
    var implementation = new Service();
    bool result = implementation.DoSomething();
    Assert.IsTrue(result);
}

当我第一次编写测试时,我不知道此服务的依赖项,因此构造函数不带任何参数,因为我不需要注入任何依赖项。

但是,在编写实现时,我意识到我需要某种依赖性,因此在构造函数中添加对该依赖性的引用。为了保持测试代码编译和失败,我必须返回测试,并修改它以创建一个假的实现。

public class Service : IService {
    public Service(IDependency dependency) {
        _dependency = dependency;
    }
    bool DoSomething() {
        ... use _dependency ...
        return result;
    }

}

[TestMethod]
public void ServiceImplementationTest()
{
    var implementation = new Service(*** new MockDependency() ***);
    bool result = implementation.DoSomething();
    Assert.IsTrue(result);
}

这只是生活中的事实吗?在编写测试之前我应该​​知道所有的依赖关系吗?当我想编写一个具有不同依赖项的新实现时会发生什么,是否有必要为每个实现编写一个新的测试,即使实现的正确性没有改变?

Should you know your dependencies in advance for each test

不一定,没有。

你在设计的时候"test first"就是在探索一个可能的API。所以下面的代码

var implementation = new Service();
bool result = implementation.DoSomething();
Assert.IsTrue(result);

public API 应该允许你创建一个 Service 的实例它的依赖项。

However, as I write the implementation, I realise I need a certain dependency, and so add a reference to that dependency in the constructor. To keep the test code compiling and failing, I then have to go back to the test, and modify it to create a fake implementation.

所以请注意这里的两件事

  1. 这不是向后兼容的更改...
  2. 这意味着它不是重构

所以你的部分问题是你正在以一种向后不兼容的方式引入你想要的更改。如果您重构,您的构造函数将类似于

public Service() {
    this(new IDependency() {
        // default IDependencyImplementation here
    });
}

Service(IDependency dependency) {
    this.dependency = dependency;
}

此时,您有两个独立个决定

  • 应该 Service(IDependency) 是 public API
  • 的一部分

如果是这样,那么您将开始编写强制您公开该构造函数的测试

  • 应该 Service() 被弃用

如果应该,那么当您从 public API 中删除 Service() 时,您计划 删除依赖它的测试 ].

注意:如果您还没有 shared/released public API,那么有很多不必要的步骤。除非你故意采用校准测试是不可变的原则,否则破解测试以反映你的 API.

的最新草稿通常更实际。

不过不要忘记在更改后重新校准测试;对测试的任何更改都应该触发 Red/Green 循环的刷新,以确保修订后的测试仍在衡量您的预期。您永远不应发布未经校准的测试。

有时有用的模式是将被测系统的组成与自动检查分开

public void ServiceImplementationTest()
{
    var implementation = new Service();
    check(implementation);
}

void check(Service implementation) {
    bool result = implementation.DoSomething();
    Assert.IsTrue(result);
}

另一种选择是写一个 spike - 一个没有测试的可能实现的草案,目的是探索以更好地理解问题的约束, 扔掉学习练习完成时。

Could you quickly describe what you mean by test calibration? Google isn't helping much. Is it making sure that tests fail when they are supposed to?

是:确保在他们认为的时候失败,确保他们失败时报告的消息是合适的,当然还要确保他们在什么时候通过他们应该。

我写了一些关于这个想法的文章 here