如何对顺序逻辑进行单元测试?

7
假设我有一个名为Car的类,包含以下方法:
  • LoadGasoline(IFuel gas) 加油
  • InsertKey(IKey key) 插入钥匙
  • StartEngine() 启动引擎
  • IDrivingSession Go() 开始行驶
Car的目的是配置并返回一个IDrivingSession,应用程序的其余部分将使用它来驾驶汽车。如何对我的Car进行单元测试?
看起来需要在调用Go()方法之前完成一系列操作。但是我想要分别测试每个方法,因为它们都有一些重要的逻辑。我不想有一堆像这样的单元测试:
Test1: LoadGasoline, Assert

Test2: LoadGasoline, InsertKey, Assert

Test3: LoadGasoline, InsertKey, StartEngine, Assert

Test4: LoadGasoline, InsertKey, StartEngine, Go, Assert

有没有更好的方法来对顺序逻辑进行单元测试?还是这是我的汽车设计存在问题?

--- 编辑 ---- 感谢所有回答。正如许多人注意到的那样,我还应该测试无效情况,而我也有这些测试,但这个问题侧重于如何测试有效的顺序。

6个回答

3
我认为每个方法都应该单独和独立地进行测试。
在我看来,你应该为每种情况准备环境,这样只有当你更改LoadGasoline方法时,LoadGasoline测试才会失败,而不需要因为一个错误而看到所有测试失败。
我不知道你的汽车状态是什么样子的,但是,在插入钥匙之前,你应该准备一个像car.SetTotalGasoline(20)这样的方法,或者设置在这个方法中的任何变量,但不要依赖于LoadGasoline方法的复杂逻辑。
稍后,你将需要进行一项测试(在这种情况下,不是单元测试),以测试所有的顺序。

但是如果您必须初始化对象才能将汽车置于正确的状态,那么您最终会调用LoadGasoline方法。 如果您修改了实现,可能不仅会破坏其单元测试,而且会影响所有其他测试的设置代码。 - JoshBerke
你可以扩展汽车类来实现这一点。比如,创建一个继承 Car 类的辅助测试类,并添加更多方法以帮助测试。 - Samuel Carrijo
我最喜欢这种方法,它似乎非常符合我对良好设计和单元测试的理念... 如果没有更好的方法出现,稍后将标记为答案。 - Andriy Volkov

2
一些单元测试框架允许您指定设置代码,在实际测试开始之前运行。
这使您能够在运行测试之前将目标对象置于适当的状态。这样,您的测试可以根据您正在测试的特定代码而不是在运行测试之前所需的代码来确定是否通过或失败。
因此,您的测试序列将类似于以下内容:
Test1: 
    LoadGasoline, Assert

Test2 Setup:
    LoadGasoline

Test2:
    InsertKey, Assert

Test3 Setup: 
    LoadGasoline, InsertKey

Test3:
    StartEngine, Assert

Test4 Setup: 
    LoadGasoline, InsertKey, StartEngine

Test4:
    Go, Assert

现实情况是,由于所有测试都按顺序运行,如果前一个测试通过,则不存在测试的设置会失败的可能性。

尽管如此,您还应该测试预计不能正常工作的失败案例,但这是不同的问题。


这是我最初的做法,但是(至少对于 MSTest)如果单元测试的设置逻辑失败,则单元测试将失败,因此结果与我在每个方法中都有所有步骤的结果相同。 - Andriy Volkov

1

为什么你不想要所有这些测试呢?

Go 在调用之前或之后,例如 InsertKey,行为会有很大的不同,对吧?所以我认为你应该测试这两种行为。


我认为他的问题在于Test 4测试的内容与测试1、2和3相同,因为所有操作都是按照相同的顺序完成的。他没有在两个不同的测试中在插入密钥之前或之后调用Go。我同意原帖中Test 4重复了测试1-3的内容。而且我也同意您所说需要有不同的顺序变化。这就像你试图开一辆没启动的车。 - JoshBerke

1

这是一种公平的不情愿,但有时这就是你需要做的。如果您无法欺骗测试系统,使其认为它处于较晚的状态,则需要按照相同的步骤将其置于该状态。如果不了解更多关于您正在测试的内容,那么如何欺骗不同状态就不清楚了。

您可以使用提取方法重构来处理一个状态的测试,以便可以使用相同的代码来准备下一个测试,从而使这种情况变得可容忍。


1

我可能会有

Test 1: LoadGasoline, Assert, InsertKey Assert, StartEngine Assert, Go Assert
Test 2: LoadGasoline, Go, Assert
Test 3: Go, Assert
Test 4: StartEngine, Go, Assert

根据实际对象的情况,我可能不会尝试所有的排列组合,但是我会有一个单一的测试来验证成功的轨迹,然后我会测试那些命中我的边缘情况。
编辑:
经过一些思考,我可能会有以下测试:
  • 用没有油的钥匙启动汽车
  • 用错误的钥匙启动加了油的汽车
  • 用正确的钥匙启动加了油的汽车(上述第一个测试)
  • 在启动汽车之前按下油门。

+1 这更像是行为驱动开发,这可能是他想要关注的。 - Jeffrey Cameron
如果LoadGasoline以后重构时出现了一个错误,该怎么办?在您的示例中,Test1和Test2都将失败,这是我认为应该避免的。 - Andriy Volkov
是的,它会失败。问题在于你想要测试更多的集成风格行为还是更纯粹的隔离测试。要进行隔离测试,你需要确保能够将对象设置到正确的状态。根据你的模型,这可能有效也可能无效。 - JoshBerke

0

从技术上讲,您至少应该使用以下测试:

testLoadGasoline

testInsertKeyGasolineNotLoaded

testStartEngineKeyNotInserted

testGoEngineNotStarted

testGo

如果您可以直接查看中间步骤,那么您可以添加

testInsertKeyGasolineLoaded

testStartEngineKeyInserted

请注意,如果您可以直接设置状态(这取决于语言和设计),那么testInsertKeyGasolineLoaded可能实际上并不会调用LoadGasoline

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接