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

时间:2024-12-31
  什么是TDD?
  测试驱动开发(TDD)是编写软件的迭代过程,其中单元测试是在实现之前开发的。这是一个紧密的反馈循环,由以下步骤组成:
  编写一个单元测试,看着它失败。
  编写足够的代码来通过测试。
  改进代码(不改变其行为)。
  这些步骤通常被称为“红色、绿色、重构”,表示测试从失败(红色)到通过(绿色)的过程,有机会改进代码和测试(重构)。在开发过程中,这个循环会一遍又一遍地重复数百或数千次。
  在此过程中,编写测试是驱动 软件开发的动力。在编写代码之前,您需要考虑代码要做什么,然后将这个想法保存在单元测试中。只有这样你才能编写下一段代码。这迫使你非常清楚你想要代码做什么。
  每通过测试,您就会对软件正常运行更有信心。而且,由于每一段代码都是由测试驱动的,因此您终会获得很大的测试覆盖率——通过单元测试测试的代码量。
  不要浪费时间编写不可测试的代码
  单元测试的问题之一(尤其是当您刚刚开始时)是您终可能会编写难以测试的代码。
  例如,也许您需要访问一些内部状态,但您不想公开它。或者,您的被测单元可能有许多难以模拟的复杂依赖项。
  编写可测试的代码需要经验,但如何才能获得经验呢?好吧,事实证明, 如果您从 TDD 开始,您就不需要这种经验。 当您首先编写测试时,您就无法编写不可测试的代码。
  您将从一开始就取得成功,因此您将更有可能实际采用单元测试作为实践。想象两个场景:
  场景 1:您编写了一大堆代码,然后尝试找出如何测试它。当您无法快速弄清楚时,您就会放弃,因为您还有软件要交付!也许您会学到一些有关如何使您的代码下次更易于测试的知识。
  场景 2:您有一个要创建的软件模块的想法,但您不确定如何测试它。因此,您花了一些时间来弄清楚如何编写个测试。然后你编写一些代码使其通过。好吧!您刚刚编写了个单元测试。干得好,你刚刚学到了一些东西。重复此操作,直到获得经过全面单元测试的模块。恭喜...您刚刚学到了很多有关单元测试的知识。
  TDD是一个体验放大器。你边做边学。 TDD 鼓励您做正确的事情,以便您学得更快。你学得越多,你就越能更好地编写单元测试。
  测试驱动的心态
  测试时,您会以稍微不同的方式思考您正在编写的代码。您不必尝试跟踪  您希望软件执行的 所有操作,而只需担心 您希望软件执行的下一操作。让我们看一个例子来说明。
  我喜欢讨论 TDD 的例子之一是 命令解析器,因为它被用在很多嵌入式系统中。通常,您希望您的系统能够与外界对话,以便它实际上可以做有趣的事情。这可能只是一个用于配置的简单串行接口,也可能是与其他设备或互联网的连接。
  根据我的经验,这些类型的接口确实可以从单元测试中受益。它们通常是定制的,并且很快就会变得复杂——有许多代码路径和许多需要处理的错误情况。而且,由于这是系统的外部接口,因此您不能总是期望另一端的人表现良好。不过,通过一些单元测试,您可以确保一切按预期工作——并且所有错误情况都得到处理。
  考虑一个带有简单命令解析器的嵌入式系统。它从某个地方(例如串行或 USB,但我们的解析器实际上并不关心)接收字符流,并在收到特定字符序列时执行某些操作。在这种情况下,系统中有一个可以控制的扬声器。
  大多数嵌入式软件开发人员的反应是开始在 command_parser.c 中编写一大堆代码。测试驱动的方法是不同的。
  步是: 编写测试,观察它失败。为了编写测试,您需要弄清楚您希望命令解析器执行的件事。如果有协议规范(哈,对!),您可以看一下。如果没有,您现在可以决定代码首先要做什么。这个怎么样?
  当收到“m”字符时,扬声器将静音。
  好吧,这是一个简单、小且定义明确的功能。让我们编写一个单元测试,如果执行此操作的代码已实现,则该测试将通过。
  #include "some_test_framework.h"
  #include "some_mock_framework.h"
  #include "command_parser.h"
  #include "mock_speaker.h"
  // A test for the command_parser.
  void test_WhenAnMIsReceived_ThenTheSpeakerIsMuted(void)
  {
  // Receive an "m."
  command_parser_put_char('m');
  // Make sure the mute function is called.
  EXPECT_CALL(speaker_mute());
  }
  哇,这只是一个测试,但这里有很多设计决策。
  为命令解析器定义了一个新函数:command_parser_put_char()。这就是将字符输入命令解析器的方式,以及如何传入“m”进行测试的方式。
  扬声器模块还定义了另一个新功能:speaker_mute()。这将实现扬声器的实际静音。当这个函数被调用时,你就知道测试已经通过了。
  由于这是一个单元测试,command_parser将被单独测试,并且不会调用真正版本的speaker_mute()。相反,将提供一个模拟函数(可能包含在 中mock_speaker.h),并且该EXPECT_CALL宏是使用任何模拟机制的替代品。如果speaker_mute()未调用该函数,测试将失败。
  请注意,这些功能实际上还不存在。但是......您刚刚定义了您想要的确切行为,并且您有一个明确的方法来测试它。如果你现在运行测试,它肯定会失败。事实上,它会编译失败,因为函数不存在。
  现在进行第二步:编写足够的代码来通过测试。终于到了写一些代码的时候了!这是command_parser_put_char()使测试通过所需的简单的代码 :
  // Receive a character.
  void command_parser_put_char(char next_char)
  {
  speaker_mute();
  }
  请注意,您还需要为speaker_mute().详细信息取决于您在项目中如何使用模拟。
  现在测试应该通过了……但请注意,我们甚至没有检查我们收到的是哪个字符!这可能看起来有点愚蠢,但 TDD 的目标之一是化未完成的工作量。
  现在这是一个简单的例子。然而,当代码变得更加复杂时,任何您 实际上没有 编写的代码都会使您的应用程序更简单、更容易理解(咳咳……更好)。当你只做你需要做的工作时,关心日程和预算的人也会更高兴。
  TDD 周期的一步是重构,即 在不改变代码行为的情况下改进代码。此步骤的关键是您已经有了验证行为的单元测试。因此,您可以自由地尝试更改代码,因为失败的测试会立即告诉您是否更改了行为。不过,由于这只是次测试,因此还没有太多需要改进的地方。
  命令解析器的其余部分是通过重复 TDD 循环来实现的。那么,您希望命令解析器下一步做什么?怎么样:
  当收到“u”字符时,扬声器将取消静音。
  好吧,这又是一件好事。这是一个测试:
  void test_WhenAUIsReceived_ThenTheSpeakerIsUnmuted(void)
  {
  // When
  command_parser_put_char('u');
  // Then
  EXPECT_CALL(speaker_unmute());
  }
  当您改进命令解析器实现以通过测试时,它可能看起来像这样:
  void command_parser_put_char(char next_char)
  {
  if (next_char == 'm')
  {
  speaker_mute();
  }
  else
  {
  speaker_unmute();
  }
  }
  现在处理错误情况怎么样?如果收到意外字符怎么办?
  当接收到意外字符时,扬声器静音状态不变。
  void test_WhenAnUnexpectedCharIsReceived_ThenTheSpeakerMuteStateIsUnchanged(void)
  {
  // When
  command_parser_put_char('!');
  // Then
  DO_NOT_EXPECT_CALL(speaker_mute());
  DO_NOT_EXPECT_CALL(speaker_unmute());
  }
  这里的代码足以让这个测试通过:
  void command_parser_put_char(char next_char)
  {
  if (next_char == 'm')
  {
  speaker_mute();
  }
  else if (next_char == 'u')
  {
  speaker_unmute();
  }
  }
  这里还有什么你想重构的吗?如果您更喜欢 switch 语句,您可以继续更改它:
  void command_parser_put_char(char next_char)
  {
  switch(next_char)
  {
  case 'm':
  speaker_mute();
  break;
  case 'u':
  speaker_unmute();
  break;
  default:
  break;
  }
上一篇:电压可以为负吗了解负电压
下一篇:利用 SiC 和 GaN 电源满足 AI 需求

免责声明: 凡注明来源本网的所有作品,均为本网合法拥有版权或有权使用的作品,欢迎转载,注明出处。非本网作品均来自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

相关技术资料