如何对代码进行分解以简化可测试性?

时间:2009-12-01 05:00:21

标签: c++ unit-testing tdd

我正在学习单元测试,并想知道如何编写可测试的代码。但是,我不确定如何在不使其复杂的情况下编写可测试代码。我将采用着名的汽车和发动机问题来描述问题。

class Car
{
private:
   Engine m_engine;

public:
   Car();
   // Rest of the car
}

我提出了以下解决方案,以使上述代码可测试。

  1. 更改Car的构造函数以将Engine作为参数。然后模拟引擎并进行测试。但是,如果我没有不同类型的引擎,那么为了使其可测试而参数化构造函数似乎是不合适的。

  2. 使用setter然后将模拟引擎传递给setter。与上述相同的流程。

  3. 首先测试引擎,然后使用经过验证的引擎(或使用存根引擎)测试汽车。

  4. 我必须在代码上测试哪些替代方案?每种方法的优点和缺点是什么?

8 个答案:

答案 0 :(得分:7)

从不同的(测试驱动开发)观点来看:易于测试的代码易于使用。编写单元测试实际上是在测试代码的“公共接口”。如果它很难测试,那是因为你在那里有一些依赖,这使得它很难。你真的需要一个遏制关系,还是一个联想关系会更有意义?

在你的情况下,我个人认为在构造函数中传递Engine会更容易测试,所以我会像你的建议#1那样重构构造函数。您可以在一个测试套件中测试引擎,并提供一个模拟引擎来测试另一个测试套件中的Car。现在测试它很简单,这意味着界面易于使用。这是好事。

现在考虑如何在实际项目中使用该实现。您将创建一个CarFactory类,工厂将创建一个引擎并将其放入Car中,然后再将其交付给您。 (还要注意这最终会如何更接近地塑造汽车,发动机和工厂的现实世界,但我离题了。)

因此,TDD的答案是重构代码以在构造函数上获取引擎指针。

答案 1 :(得分:5)

如果您只有一种引擎类型,为什么要尝试将其作为新对象?如果您不打算交换引擎,请不要创建另一个抽象层。只需将发动机作为汽车的一部分。

您可能正在分解以降低复杂性,而不是重用组件。好决定。在这种情况下,我会说3是你最好的选择 - 验证你的低级组件,然后使用调用较低级别对象的更高级代码。

实际上,Engine更像是数据库。并且您将需要更改构造函数以使用不同的数据库(出于测试原因或其他原因),但您可以暂时搁置该谎言。

答案 2 :(得分:2)

单位测试,顾名思义,是关于测试单位以确认他们按照规定工作。这意味着,您应该单独测试引擎。

系统测试或集成测试是关于测试它们是否正确“粘合”在一起。

当然,它比这更复杂,但它应该指向正确的方向。

答案 3 :(得分:2)

选项1通常是正确的方式。

通过完全控制您给汽车的发动机,您可以很好地测试汽车。

您可以使用引擎为汽车提供的所有不同输出,更轻松地测试汽车的行为方式。您还可以确保汽车对发动机进行适当的调用。

将它放在构造函数中会非常清楚Car依赖引擎来工作。将它与依赖注入框架一起使用,构造函数问题根本不是问题。

答案 4 :(得分:2)

真正的问题是,有哪些要求?如果目标是简单地实现“Car”对象,那么它甚至不需要引擎。

测试应始终与要求相关。任何对象模型都将是对现实的一些概括,因此问题在于需要表示哪些方面。

一旦你的要求下降,那么你应该在通用的高级别上编写你的测试。然后,OO设计应该以可以实现这些测试的方式完成。

答案 5 :(得分:2)

Misko Hevery经常写这个话题。从2009年10月开始Here's a presentation。他认为依赖图应该在构造函数中是显式的。

  

似乎不适合参数化构造函数只是为了使其可测试

我认为这是一个引人入胜的评论。在什么样的条件下,包括可测性作为设计的一部分是不合适的?牺牲可测试性的正确性显然是错误的,尽管在实践中我从未见过这种选择被迫。牺牲可测性的性能......也许在某些特定情况下。牺牲代码一致性?我个人宁愿改变编码标准,并逐渐将遗留代码纳入合规范围。

答案 6 :(得分:1)

C ++的一个可能性是使用友谊:

class Car
{
private: //for unit testing
    friend class TestCar; //this class is the unit test suite for the Car class
    Car(Engine* mockEngine); //this constructor is only used by the TestCar class
private:
    Engine* m_engine;
public:
    Car();
    // Rest of the car
};

第二种可能性是构造函数的实现使用全局/静态方法,例如如下所示,您可以通过一些配置文件或通过链接(可能是动态链接)来更改此方法的实现此方法的版本:

Car::Car()
{
    m_engine = Engine::create();
}

答案 7 :(得分:0)

我赞成允许(通过构造函数或属性)将Engine的实现添加到Car(最好是IEngine)。

汽车测试实际上不应该关心引擎的作用,只要它能够正确响应调用引擎的结果。然后,使用假引擎进行测试,以便您可以控制发送到汽车的信号,你应该好好去。