34

我有一种情况,我需要为嵌入式硬件的一些设备驱动程序编写一些单元测试。代码很旧而且很大,不幸的是没有很多测试。目前,唯一可能的测试是完全编译操作系统,将其加载到设备上,在现实生活场景中使用它并说“它可以工作”。没有办法测试单个组件。

我在这里遇到了一个不错的线程,它讨论了嵌入式设备的单元测试,我从中获得了很多信息。我想更具体一点,并询问是否有人在这种情况下测试设备驱动程序有任何“最佳实践”。我不希望能够模拟相关板正在与之交谈的任何设备,因此可能必须在实际硬件本身上对其进行测试。

通过这样做,我希望能够获得驱动程序的单元测试覆盖率数据,并哄骗开发人员编写测试以增加驱动程序的覆盖率。

我想到的一件事是编写在操作系统上运行的嵌入式应用程序并运行驱动程序代码,然后将结果传达回测试工具。该设备有几个接口,我可以使用它们来驱动我的测试 PC 上的应用程序,以便我可以练习代码。

任何其他建议或见解将不胜感激。


更新:虽然它可能不是准确的术语,但当我说单元测试时,我的意思是能够测试/执行代码而无需编译整个 OS+驱动程序并将其加载到设备上。如果我必须这样做,我会称之为集成/系统测试。

问题是我们拥有的硬件是有限的,开发人员经常在修复错误等时使用它们。保持一个专用并连接到完成 CI 服务器和自动化测试的机器可能是不可以的这个阶段。这就是为什么我正在寻找方法来测试驱动程序,而不必实际构建整个东西并将其上传到设备上。


概括

基于以下出色的答案,我认为解决该问题的合理方法是使用 IOCTL 公开驱动程序功能,然后在嵌入式设备的应用程序空间中编写测试以实际运行驱动程序代码。

将一个小程序驻留在设备上的应用程序空间中也很有意义,该程序公开了一个 API,该 API 可以通过串行或 USB 运行驱动程序,以便可以在 PC 上编写单元测试的内容,该 PC 将与硬件并运行测试。

如果项目刚刚开始,我认为我们可以更好地控制组件的隔离方式,以便主要在 PC 级别进行测试。鉴于编码已经完成并且我们正在尝试将测试工具和案例改进到系统上,我认为上述方法更实用。

谢谢大家的回答。

4

6 回答 6

9

在过去,这就是我们测试和调试设备驱动程序的方式。调试此类系统的最佳方式是工程师将嵌入式系统用作开发系统,并且一旦达到足够的系统成熟度,就可以取消原来的交叉开发系统!

对于您的情况,我想到了几种方法:

  • 添加 ioctl 处理程序:每个代码都执行特定的单元测试
  • 使用条件编译,将 main() 添加到驱动程序中,在驱动程序中进行功能单元测试并将结果输出到stdout.
  • 为了便于调试,也许这可以使多平台可操作,这样您就不必在目标硬件上进行调试。
  • 也许条件代码也可以模拟回送式设备。
于 2009-12-24T08:05:15.243 回答
7

真正依赖于硬件的代码(分层架构中驱动程序堆栈的最低级别)实际上无法在任何地方进行测试,除非在硬件或硬件的高质量模拟上进行。

如果您的驱动程序具有一些不直接依赖于硬件的高级功能组件(例如,用于以特定格式向硬件发送消息的协议处理程序)并且如果该部分在代码中很好地独立,那么您可以在基于 PC 的单元测试框架中单独对其进行单元测试。

回到最底层——如果它依赖于硬件,那么测试夹具需要包含硬件。您可以制作一个包含硬件、驱动程序和一些测试软件的测试夹具。我认为最主要的是将正常产品的应用程序代码从测试中取出,然后放入一些测试代码。测试代码可以系统地测试驱动程序的所有功能和极端情况(应用程序代码可能不会),并且还可以在很短的时间内真正密集地测试驱动程序(应用程序可能不会)。因此,它比仅运行应用程序更有效地使用有限的硬件,并为您提供更好的结果。

如果您可以让 PC 进入循环,那么 PC 可能有助于测试。例如,如果您正在为嵌入式设备编写串行端口驱动程序,那么您可以:

  • 为发送各种已知数据流的嵌入式设备编写测试代码。
  • 将其连接到 PC 的串行端口,运行验证传输数据流的测试代码。
  • 另一个方向相同——PC发送数据;嵌入式设备接收并验证它,并通知 PC 任何错误。
  • 测试可以全速传输数据,并使用一系列不同的字节时序(我曾经发现一个微控制器 UART 硅错误,该错误仅在字节之间以约 5 毫秒的延迟发送字节时出现)。

您可以使用以太网驱动程序、Wi-Fi 驱动程序做类似的事情。

如果您正在测试存储设备驱动程序,例如 EEPROM 或闪存芯片,那么 PC 就无法以同样的方式参与其中。在这种情况下,您的测试工具可以测试各种写入条件(单字节、块...),并使用各种读取条件验证数据完整性。

于 2009-12-24T23:54:23.713 回答
3

两三年前我也遇到过类似的问题。我已将设备驱动程序从 VxWorks 移植到 Integrity。我们只更改了驱动程序的操作系统相关部分,但这是一个安全关键项目,因此所有单元测试、集成测试都重做。我们在单元测试中使用了一个名为 LDRA 测试平台的自动化测试工具。我们 99% 的单元测试都是在使用 Microsoft 编译器的 Windows 机器上完成的。现在我将解释如何做到这一点

首先,当您进行单元测试时,您正在测试软件。当您在测试中包含真实设备时,您也在测试该设备。有时硬件或硬件文档可能存在问题。当你在设计软件的时候,如果你已经清楚地描述了每个功能的行为,那么进行单元测试是很容易的,例如,考虑功能;

readMessageTime(int messageNo, int* time); 
//This function calculates the message location, if the location is valid, 
//it reads    the time information 
address=calculateMessageAddr(messageNo); 
if(address!=NULL) { 
    read(address+TIME_OFFSET,time); 
    return success; 
} 
else { 
return failure; 
} 

好吧,在这里你只是在测试 readMessageTime 是否正在做它应该做的事情。您不必测试 calculateMessageAddr 是否正在计算正确的结果,或者 read 读取正确的地址。那是其他一些单元测试的责任。所以你要做的是为 calculateMessageAddr 和 read(OS function) 编写存根,并检查它是否调用具有正确参数的函数。如果您不直接从驱动程序访问内存,就是这种情况。抱着这种心态,您可以在没有任何操作系统或设备的情况下测试任何类型的驱动程序代码。

如果您已将设备内存直接映射到您的内存空间,并且设备驱动程序读取和写入设备内存,因为它是它自己的内存,它会变得有点复杂。使用自动化测试工具,现在您必须观察指针的值并根据这些指针的值定义通过/失败标准。如果您正在从内存中读取值,则必须定义预期值。在某些情况下,这可能很难。

还有一个问题,开发人员总是对驱动程序的单元测试感到困惑,例如:

 readMessageTime(int messageNo, int* time); 
 //This function calculates the message location, if the location is valid,
 //it does some jobs to make the device ready to read then 
 //it reads the time information 
 address=calculateMessageAddr(messageNo); 
 if(address!=NULL) { 
      do_smoething(); // Get the device ready to read!    
      do_something_else() // do some other stuff so you can read the result in 3us.
      status=NOT_READY;
      while(status==NOT_READY) // mustn't be longer than 3us.
           status=read(address+TIME_OFFSET,time); 
      return success; 
  } else 
  { 
  return failure; 
  } 

这里 do_something 和 do_something_else 在设备上做一些工作以使其准备好读取。开发人员总是问自己“如果设备没有永远准备好并且我的代码在这里出现死锁怎么办”,他们倾向于在设备上测试这种东西。

好吧,您必须信任设备制造商和技术作者。如果他们说设备将在 1-2us 内准备好,您无需担心这一点。如果您的代码在这里失败,您必须将其报告给设备制造商,找到解决此问题的解决方法不是您的工作。你明白我的意思了吗?

我希望这有帮助…。

于 2009-12-26T16:50:11.777 回答
3

就在两个月前,我有这个确切的任务。

让我猜猜:您可能有一些代码“片段”向设备讲述低级细节。您知道这些片段可以工作,但您无法了解它们,因为它们依赖于设备驱动程序。

同样,单独测试它的每一行也没有意义。它们永远不会孤立运行,您的单元测试最终会看起来像生产代码的镜像。例如,如果你想启动设备,你需要创建一个连接,向它传递一个特定的低级重置命令,然后是一个初始化参数结构等。如果你需要添加一个配置,这可能需要你要使其脱机,请添加配置,然后使其联机。类似的东西。

你不想测试低级的东西。然后,您的单元测试将仅反映您假设设备如何工作而无需确认任何内容。

这里的关键是创建三个项目:控制器、抽象和该抽象的适配器实现。在 Cpp、Java 或 C# 中,您将创建一个基类或一个接口来表示此抽象。我将假设您创建了一个界面。您将片段分解为原子操作。例如,您在界面中创建一个名为“start”和“add(parameter)”的方法。你把你的片段放在设备适配器中。控制器通过接口作用于适配器。

识别您放置在适配器中的片段中的逻辑片段。然后你需要决定这个逻辑是低级的(协议处理细节等)还是应该属于控制器的逻辑。

然后您可以分两个阶段进行测试: * 有一个作用于具体适配器的简单测试面板应用程序。这用于确认适配器实际工作。当您按“开始”时它开始。例如,如果您依次按下“下线”、“传输(192)”和“上线”,则设备会按预期响应。这是您的集成测试。

您不对适配器中的详细信息进行单元测试。您手动测试它,因为唯一的成功标准是设备如何响应。

但是,控制器是完全单元测试的。它只依赖于抽象,它在您的测试代码中被模拟出来。因此,您的代码不依赖于您的设备驱动程序,因为不涉及具体适配器。

然后你编写单元测试来确认,例如,方法“Add(1)”实际上调用了“Go offline”,然后是“Transmit(1)”,然后是模拟抽象的“Go online”。

这里的挑战是区分适配器和控制器。什么去哪里?对我有用的是首先创建上述测试面板,然后通过它操作设备。

适配器应隐藏您仅在设备更改时才需要更改的详细信息。

  1. 如果控制面板操作繁琐,需要一次又一次地重复大量序列,或者操作面板需要非常特定的设备知识,那么您的粒度太高,应该将其中一些组合在一起。测试面板应该是有意义的。

  2. 如果最终用户需求变化对适配器代码有影响,那么您的粒度可能太低,应该拆分操作,以便控制器类中的测试驱动开发可以适应需求变化。

于 2010-01-07T18:37:56.913 回答
2

我建议进行基于应用程序的测试。即使脚手架建造起来既困难又昂贵,这里也有很多收获:

  • 只崩溃一次进程而不是一个系统
  • 使用标准工具集的能力(调试器,内存检查器......)
  • 克服硬件可用性限制
  • 更快的反馈:设备中无需安装,只需编译和测试
  • ...

就命名而言,这可以称为组件测试。

应用程序可以像目标操作系统一样初始化设备驱动程序,也可以直接使用驱动程序的实习生。前者更贵,但覆盖面更大。然后链接器将告诉哪些函数丢失,存根它们,可能使用爆炸存根

于 2009-12-24T13:24:12.683 回答
1

词汇

我不希望能够模拟相关板正在与之交谈的任何设备,因此可能必须在实际硬件本身上对其进行测试。

然后,您将退出单元测试。也许您可以改用其中一种表达方式?

  • 自动化测试:无需用户输入即可进行测试(与手动测试相反)。
  • 集成测试:一起测试几个组件(与单元测试相反)。
    在更大的范围内,如果您测试整个系统而不仅仅是几个组件一起测试,则称为系统测试。

在问题的评论和更新后添加:

  • 组件测试:类似于集成测试或系统测试,但规模更小。
    注意:所有三个组件-集成-系统测试都有相同的问题集,但规模不同。相反,单元测试没有(见下文)。

“真实”单元测试的优势

对于集成(或系统或组件)测试,获得一些反馈当然很有趣,比如测试覆盖率。这样做当然有用。

但是在某个点上取得进展是非常困难的(读作“非常昂贵”),所以
我建议你使用互补的方法,比如添加一些真正的单元测试。为什么?:

  • 很难模拟边缘或错误条件。(例如:计算机时钟在交易过程中跨越一天或一年;网络电缆被拔掉;某些组件或整个系统断电然后又启动;磁盘已满)。使用单元测试,因为您模拟这些条件而不是尝试重现它们,所以要容易得多。单元测试是你获得非常好的代码覆盖率的唯一机会。
  • 集成测试需要时间(因为要访问外部资源)。您可以在执行一个集成测试期间执行数千个单元测试。因此,只有使用单元测试才能测试许多组合......
  • 需要访问特定资源(硬件、许可证等),集成测试通常在时间或规模上受到限制。如果资源由其他项目共享,则每个项目可能仅在每天几个小时内使用它们。即使具有独占访问权限,也可能只有一台机器可以使用它,因此您无法并行运行测试。或者,您的公司可能购买了用于生产的资源(许可证或硬件),但没有(或足够早)用于开发......
于 2009-12-24T08:06:53.017 回答