OpenVINOTM,给你看得见的未来!>>
电子产品世界 » 论坛首页 » 综合技术 » 基础知识 » 如何用TDD编写更好的嵌入式软件单元测试?

共1条 1/1 1 跳转至

如何用TDD编写更好的嵌入式软件单元测试?

助工
2018-07-09 19:02:42    评分
想尝试对嵌入式软件进行单元测试吗?这就是你应该从TDD开始的原因。

单元测试可以帮助您编写更好的嵌入式软件。如果您对自己的单元测试的好处感兴趣,那么您应该从测试驱动开发(TDD)开始。

什么是TDD?

测试驱动开发(TDD)是编写软件的迭代过程,其中单元测试是在实现之前开发的。这是一个紧密的反馈循环,包括以下步骤:

  1. 写一个单元测试,看它失败。

  2. 编写足够的代码来通过测试。

  3. 改进代码(不改变其行为)。

对于测试从失败(红色)到传递(绿色)的方式,这些步骤通常被称为“红色,绿色,重构”,最后有机会改进代码和测试(重构)。在开发期间,该循环一遍又一遍地重复数百或数千次。

 


在这个过程中,编写测试是推动  软件开发的动力您可以在编写代码之前考虑代码要执行的操作,并将该想法保存在单元测试中。只有这样你才能编写下一段代码。这迫使您非常清楚您希望代码执行的操作。

通过每次通过测试,您可以更自信地确保软件正常运行。而且,由于每一段代码都是由测试驱动的,因此最终会得到很好的测试覆盖率 - 使用单元测试测试的代码量。

 

不要浪费你的时间编写不可测试的代码

单元测试的一个问题 - 特别是当你刚刚开始时 - 是你可能最终编写难以测试的代码。

例如,您可能有一些需要访问的内部状态,但您不想公开它。或者,您测试的单元可能存在许多难以模拟的复杂依赖项。

编写可测试的代码需要经验,但是你怎么能得到它?嗯,事实证明,如果你从TDD开始,  你不需要那种经验。 首先编写测试时,不能编写不可测试的代码。

你从一开始就会成功,所以你更有可能真正采用单元测试作为练习。想象一下两个场景:

场景1:您编写了大量代码,然后尝试弄清楚如何测试它。当你无法快速弄明白时,你放弃了,因为你有软件可以运送!也许你已经了解了如何让你的代码下次更容易测试。

场景2:您对某些软件模块有创意,但您不确定如何测试它。所以,你花了一点时间搞清楚如何编写第一个测试。然后你写一些代码让它通过。好的!你刚刚写了第一次单元测试。干得好,你刚刚学到了一些东西。重复,直到您有一个完全经过单元测试的模块。恭喜......你刚刚学到了很多关于单元测试的知识。

TDD是一种体验放大器你从实践中学习。TDD鼓励您做正确的事情,以便您更快地学习。学的越多,编写单元测试就越好。

以测试为导向的思维方式

在试驾时,你会想到你编写的代码有点不同。 您只需担心软件要做的下一件事  ,而不是试图跟踪您希望软件执行  的  所有操作。让我们看一个例子来说明。

讨论TDD的一个我最喜欢的例子是  命令解析器,因为它在很多嵌入式系统中使用。通常,您希望您的系统能够与外部世界交谈,以便它实际上可以做有趣的事情。这可能只是用于配置的简单串行接口,也可能是与其他设备或Internet的连接。

根据我的经验,这些类型的接口确实可以从单元测试中受益。它们通常是定制的,并且可以很快变得复杂 - 通过代码的许多路径和许多错误案例来处理。并且,由于这是系统的外部接口,因此您不能总是期望另一端的人表现得很好。通过一些单元测试,您可以确保一切按预期工作 - 并处理所有错误情况。

考虑一个带有简单命令解析器的嵌入式系统。它接收来自某个地方的字符流(例如,可能是串行或USB,但我们的解析器实际上并不关心半导体社区)并且在接收到特定字符序列时执行某些操作。在这种情况下,系统中有一个可以控制的扬声器。

 

 

大多数嵌入式软件开发人员的第一直觉是开始在command_parser.c中编写一大堆代码。测试驱动的方法是不同的。

第一步是: 写一个测试,看它失败为了编写测试,您需要弄清楚您希望命令解析器做的第一件事。如果有一个协议规范(哈,对!)你可能会看一下。如果没有,您可以立即决定您需要首先执行的代码。这个怎么样?

收到“m”字符后,扬声器静音。

好吧,这是一个简单,小巧,明确定义的功能。让我们编写一个单元测试,如果执行此代码的话,它将通过。

 image.png


哇,这只是一个单一的测试,但这里有很多设计决定。

为命令解析器定义了一个新函数:command_parser_put_char()这就是将字符输入命令解析器的方式,以及如何传入“m”进行测试。

 

 


还有另一个为扬声器模块定义的新功能:speaker_mute()这是扬声器的实际静音。您知道在调用此函数时测试已通过。

由于这是一个单元测试,因此将单独测试command_parser,并且不会调用speaker_mute()的真实版本。相反,将提供模拟函数(可能包括在内mock_speaker.h),并且EXPECT_CALL宏是用于使用任何模拟机制的替代。它将失败测试speaker_mute()是否未调用函数。

请注意,这些功能都不存在。但是......你刚刚定义了你想要的确切行为,你有一个明确的方法来测试它。如果你现在要进行测试,它肯定会失败。实际上,它将无法编译,因为函数不存在。

现在进行第二步:编写足够的代码来通过测试现在是时候编写一些代码了!以下是command_parser_put_char()进行测试通过所需的最简单的代码  
 

 image.png


请注意,您还需要设置模拟speaker_mute()具体细节取决于您在项目中如何使用模拟。

测试现在应该通过......但请注意,我们甚至不检查我们收到的字符!这似乎有点愚蠢,但TDD的目标之一是最大化未完成的工作量

现在这是一个微不足道的例子。但是,当代码变得更复杂时,任何 实际上没有编写的代码  都会使您的应用程序更简单易懂(更好)。当你只做你想做的工作时,那些关心时间表和预算的人也会更开心。

TDD周期的最后一步是重构,您 可以在不改变其行为的情况下改进代码此步骤的关键是您已经有单元测试来验证行为。因此,您可以自由地尝试更改代码,因为如果您更改了行为,失败的测试会立即告诉您。但是,由于这只是第一次测试,所以还没有太多改进。

命令解析器的其余部分通过重复TDD循环来实现。那么,你希望你的命令解析器下一步做什么?怎么样:

收到“u”字符后,扬声器将取消静音。

好吧,这是另一个好的。这是一个测试:
 image.png

 


当你改进命令解析器实现以通过测试时,它可能看起来像这样:

 image.png


现在处理错误案件怎么样?如果收到意外的角色怎么办?

收到意外字符时,扬声器静音状态不变。

 image.png

这里有足够的代码使这个测试通过:

 image.png


你有什么想重构的吗?如果您更喜欢switch语句,可以继续更改它:

image.png

嗯,这个改变是否打破了什么?没有汗,只是运行你的测试找出来。

从这里开始,您只需继续运行TDD循环 - 添加测试和功能 - 直到命令解析器执行您需要的所有操作。

作为练习,请考虑有另一个命令允许您设置音量级别。也许一个“v”后跟一个数字。你会怎么写测试?这也将引入新的错误案例。如果号码不是有效的怎么办?如果你得到一个“v”,紧接着是“m?”怎么办?您可以看到这可能很快变得复杂。但是你可以为每一个错误案例编写一个测试!在每种情况下,您都确切地知道命令解析器应该如何表现 - 如果不是,单元测试会让您知道。

将复杂问题简化为更简单的问题

构建命令解析器(或任何软件模块)是一项复杂的任务。如果您在开始之前尝试设想完成的模块,则可能很困难。如果您实现以前从未做过的事情,尤其如此,因为您没有应用任何设计模式的经验。一下子将所有这些塞进你的大脑会导致很高的认知负担

但是测试驱动的方法可以减少你的认知负担,释放你的大脑来编写一些非常棒的软件。看看我们在命令解析器示例中做了些什么。在每一步中,我们只关注要添加下一部分功能 - 而不是我们将来可能添加的所有功能将所有其他问题推迟到以后允许您将所有注意力集中在一件事情上。

工具

所以TDD很棒,对吧?困难的一个问题是嵌入式软件(即C)测试工具并不是那么好。当你在做TDD,你创建和运行测试,所有的时间这意味着您需要添加新测试并运行它们。如果这些事情都很困难,你可能会感到沮丧并放弃。

虽然它变得越来越好。Ceedling(使用Unity和CMock)等工具可以帮助您快速启动和运行。Ceedling提供自动测试发现,模拟生成和测试执行,让您的生活更轻松。

TDD未在嵌入式软件中广泛使用。如果您开始尝试使用TDD,那么您将会突破嵌入式软件开发的优势(您可以与您的应用程序和Web开发人员朋友讨论它!)。仍然有很多东西需要学习,但是你将会改进自己和你的代码。我想你会喜欢你发现的东西。




关键词: 半导体社区     IC     TDD     嵌入式软件    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]