【问题标题】:Should you know your dependencies in advance for each test您是否应该提前知道每个测试的依赖关系
【发布时间】:2018-02-01 19:52:07
【问题描述】:

我正在探索 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);
}

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

【问题讨论】:

  • 虽然是基于意见的,但依赖关系源于代码的重构。您不会总是事先知道。如果对被测主题的修改使某些测试不再相关,那么您可以简单地删除这些测试,否则需要模拟所需的依赖项。随着时间的推移,随着您的经验越来越丰富,您将倾向于找出可能需要哪些依赖项,并且在某些情况下可以事先知道可能需要哪些抽象。总而言之,这是红绿重构范式的一部分
  • 不是您的问题的答案,但我发现 a variation of the Test Data Builder pattern 对于使测试夹具可维护非常有用。

标签: unit-testing dependency-injection mocking tdd solid-principles


【解决方案1】:

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

不一定,不。

当您设计“测试优先”时,您所做的是探索可能的 API。所以下面的代码

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

表示,公共 API 应该允许您创建 Service 的实例,而无需了解其依赖关系。

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

所以请注意这里的两件事

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

因此,您的部分问题是您正在以向后不兼容的方式引入您想要的更改。如果你在重构,你会有一个步骤,你的构造函数看起来像

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

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

此时,您有两个独立的决策

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

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

  • 应弃用 Service()

如果应该,那么您计划在从公共 API 中删除 Service()删除依赖它的测试

注意:如果您还没有共享/发布公共 API,那将是很多不必要的步骤。除非您有意采用经过校准的测试是不可变的原则,否则通常更实际的做法是修改测试以反映您的 API 的最新草案。

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

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

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

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

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

您能否快速描述一下您所说的测试校准是什么意思?谷歌没有多大帮助。是否确保测试在应该失败的时候失败?

是的:确保在他们应该这样做时失败,确保当他们确实失败时报告的消息是合适的,当然还要确保他们在他们应该通过的时候通过。

我写了一些关于here的想法。

【讨论】:

  • 我喜欢关于分离 SUT 组成的最后一点。
  • 您能快速描述一下您所说的测试校准是什么意思吗?谷歌没有多大帮助。是否确保测试在应该失败的时候失败?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-11
  • 1970-01-01
  • 1970-01-01
  • 2018-07-02
  • 2011-09-05
相关资源
最近更新 更多