为有状态系统设计单元测试


20

背景

在我完成学业后,测试驱动开发得到了普及。我正在尝试学习它,但是一些主要的事情仍然无法解决。TDD的支持者说了很多类似的东西(以下称为“单一声明原则”或SAP):

一段时间以来,我一直在思考TDD测试如何尽可能简单,富有表现力和优雅。本文探讨了使测试尽可能简单和可分解的感觉:针对每个测试中的单个断言。

来源:http//www.artima.com/weblogs/viewpost.jsp? thread = 35578

他们还说这样的话(以下称为“私有方法原理”或PMP):

通常,您不直接对私有方法进行单元测试。由于它们是私有的,因此请考虑将其作为实现细节。没有人会打电话给他们中的一个,并期望它以特定的方式工作。

相反,您应该测试您的公共接口。如果调用您的私有方法的方法按预期工作,则可以假定您的私有方法正常工作。

资料来源:您如何对私有方法进行单元测试?

情况

我正在尝试测试有状态的数据处理系统。给定接收数据之前的状态,系统可以对完全相同的数据执行不同的操作。考虑一个简单的测试,该测试建立系统中的状态,然后测试给定方法要测试的行为。

  • SAP建议我不要测试“状态构建过程”,我应该假设状态是我希望从构建代码中得到的状态,然后测试我要测试的一个状态更改

  • PMP建议我不能跳过此“状态建立”步骤,而只能测试独立控制该功能的方法。

我实际代码中的结果是测试膨胀,复杂,冗长且难以编写。而且,如果状态转换发生变化,则必须更改测试……这对于小型,高效的测试是可以的,但对于这些冗长的测试却非常耗时且令人困惑。通常如何做?


2
我认为您不会为此找到理想的解决方案。通用方法不是从一开始就使系统成为有状态的,这在测试已构建的内容时无济于事。将其重构为无状态可能也不值得。
2014年


@Doval:请说明如何使电话(SIP UserAgent)变为非全状态。RFC使用状态转换图指定了该单元的预期行为。
Bart van Ingen Schenau 2014年

您是要复制/粘贴/编辑测试,还是要编写实用程序方法来共享常用的设置/拆卸/功能?尽管某些测试用例肯定会变得冗长而肿,但这不应该那么普遍。在有状态的系统中,我希望有一个通用的设置例程,其中结束状态是一个参数,并且该例程将您带到要测试的状态。另外,在每次测试结束时,我都会有一个拆卸方法,可将您带回到已知的开始状态(如果需要),因此在下一次测试开始时,您的设置方法将正常工作。
2014年

关于切线,但我还要补充一点,即使状态图在RFC中,它也是一种通信工具,而不是一项实施法令。只要您满足描述的功能,您就符合标准。我有几次将非常复杂的状态转换实现(如RFC中定义)转换为非常简单的常规处理功能。我记得有一个案例,当我意识到重命名“隐藏”的公共元素后,除了几个关于5个状态的标记所做的事情一样,我还摆脱了数千行代码。
Dunk 2014年

Answers:


15

透视:

因此,让我们退后一步,问一下TDD正在努力为我们提供哪些帮助。TDD正在尝试帮助我们确定我们的代码是否正确。正确地说,我的意思是“代码是否符合业务要求?” 卖点是我们知道将来会需要更改,并且我们希望确保在进行更改后代码保持正确。

之所以提出这种观点,是因为我认为很容易迷失在细节上,而忽视了我们要实现的目标。

原则-SAP:

虽然我不是TDD方面的专家,但我认为您缺少“单断言原则”(SAP)尝试教授的内容。SAP可以重申为“一次测试一件事”。但是,TOTAT不会像SAP那样轻松地吐槽。

一次测试一件事意味着您专注于一个案例。一条路 一个边界条件;一个错误的情况;一个无论每个测试。其背后的驱动思想是,您需要知道测试用例失败时发生了什么,以便可以更快地解决问题。如果您在一个测试中测试多个条件(即,不止一件事情),但测试失败,那么您的工作量将更大。您首先必须确定多个案例中的哪个失败,然后找出案例失败的原因。

如果一次测试一件事,则搜索范围会小很多,并且可以更快地发现缺陷。请记住,“一次测试一件事”并不一定会使您一次查看多个过程输出。例如,当测试“已知的良好路径”时,我可能希望看到一个特定的结果值foo以及中的另一个值,bar并且我可以foo != bar在测试中验证这一点。关键是根据要测试的案例对输出检查进行逻辑分组。

原则-PMP:

同样,我想您对于私有方法原理(PMP)教给我们的东西有些遗漏。PMP鼓励我们将系统视为黑匣子。对于给定的输入,您应该获得给定的输出。您不在乎黑匣子如何生成输出。您只关心输出与输入对齐。

对于查看代码的API方面,PMP确实是一个很好的视角。它还可以帮助您确定要测试的范围。确定您的接口点,并验证它们是否符合合同条款。您无需担心接口(也称为私有)方法如何完成其​​工作。您只需要验证他们做了他们应该做的事情。


已申请TDD(适合您

因此,您的情况比普通应用程序还有些皱纹。您应用的方法是有状态的,因此它们的输出不仅取决于输入,而且还取决于之前的操作。我敢肯定,我<insert some lecture>在这里应该说的是状态糟糕透顶等等,但这确实无助于解决您的问题。

我将假设您有某种状态图表,该表显示了各种潜在状态以及触发转换需要执行的操作。否则,您将需要它,因为它将有助于表达该系统的业务需求。

测试:首先,您将获得一组制定状态更改的测试。理想情况下,您将进行测试,以测试可能发生的所有状态变化,但是我可以看到一些场景,您可能不需要进行全部测试。

接下来,您需要构建测试以验证数据处理。当您创建数据处理测试时,其中一些状态测试将被重用。例如,假设您有一个Foo()基于InitState1状态具有不同输出的方法。您需要将ChangeFooToState1测试用作设置步骤,以便在“ Foo()in State1” 时测试输出。

我想提及这种方法背后的一些含义。 剧透,这是我会激怒纯粹主义者的地方

首先,您必须接受在一种情况下使用某种东西作为测试,而在另一种情况下使用某种东西。一方面,这似乎直接违反了SAP。但是,如果您从逻辑上将其定义ChangeFooToState1为具有两个目的,那么您仍将符合SAP教导我们的精神。当您需要确保Foo()更改状态时,可以将其ChangeFooToState1用作测试。而当需要验证“ Foo()”的输出时,State1ChangeFooToState1用作设置。

第二项是,从实际的角度来看,您将不需要系统的完全随机单元测试。您应该先运行所有状态更改测试,然后再运行输出验证测试。SAP是该订单背后的一种指导原则。要说明什么是显而易见的-如果某项测试失败,则不能将其用作设置。

把它放在一起:

使用状态图,您将生成测试以涵盖转换。同样,使用图表,您将生成测试以覆盖由状态驱动的所有输入/输出数据处理案例。

如果您采用这种方法,那么bloated, complicated, long, and difficult to write测试应该会更容易管理。通常,它们的结局应该更小,并且应该更简洁(即,不那么复杂)。您应注意,测试也更加分离或模块化。

现在,我并不是说该过程将完全免于痛苦,因为编写好的测试确实需要一些努力。而且其中有些仍然很困难,因为您要在很多情况下映射第二个参数(状态)。顺便说一句,为什么无状态系统更易于构建测试,这应该更加明显。但是,如果您针对应用程序采用这种方法,则应该发现自己能够证明您的应用程序正常工作。


11

通常,您会将设置细节抽象为功能,因此不必重复自己的工作。这样,只要功能更改,您只需在测试中的一个位置进行更改。

但是,您通常甚至不想将设置功能描述为describe肿,复杂或冗长。这表明您的界面需要重构,因为如果您的测试难以使用,那么您的实际代码也将难以使用。

这通常是过度投入一堂课的标志。如果您有状态要求,则需要一个类来管理状态,而无需执行其他任何操作。 支持它的类应该是无状态的。对于您的SIP示例,解析数据包应该是完全无状态的。您可以拥有一个类,该类可以解析数据包,然后调用类似的方法sipStateController.receiveInvite()来管理状态转换,而类本身可以调用其他无状态的类来执行诸如拨打电话之类的操作。

这使得为​​状态机类设置单元测试只需几个方法调用即可。如果状态机单元测试的设置需要制作数据包,则您在该类中投入了过多。同样,您的数据包解析器类应该相对简单,使用状态机类的模拟来为其创建设置代码。

换句话说,您不能完全避免状态,但是可以将其最小化和隔离。


仅作记录,SIP示例是我的,而不是来自OP。而且某些状态机可能需要多个方法调用才能使它们在特定测试中处于正确状态。
Bart van Ingen Schenau 2014年

+1表示“您无法完全避免状态,但是可以将其最小化和隔离。” 我不同意。状态是软件中必不可少的恶魔。
布兰登

0

TDD的核心思想是,通过首先编写测试,您最终会获得至少易于测试的系统。希望它能工作,可维护,有据可查等,但如果不能,那么至少它仍然很容易测试。

因此,如果您使用TDD并最终使用了难以测试的系统,则可能出了点问题。也许某些私有的东西应该是公开的,因为您需要对它们进行测试。也许您没有在正确的抽象级别上工作;像列表这样简单的东西在一个级别上是有状态的,而在另一个级别上是有状态的。或者,您可能会过多地考虑不适合您的情况的建议,否则您的问题就很难解决。或者,当然,也许您的设计很糟糕。

无论是什么原因,您都可能不会再回头再编写系统,以使其更易于通过简单的测试代码进行测试。因此,最好的计划可能是使用一些更高级的测试技术,例如:

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.