设备驱动单元测试

37
我有这样一种情况,需要为嵌入式硬件的一些设备驱动程序编写一些单元测试。代码相当老且庞大,不幸的是没有太多的测试。目前,唯一可能的测试方式是完全编译操作系统,将其加载到设备上,在实际使用场景中使用并说“它可以工作”。没有办法测试单个组件。
我找到了这里的一个很好的帖子讨论了嵌入式设备的单元测试,从中得到了很多信息。我想更具体地询问是否有人在这种情况下有任何测试设备驱动程序的“最佳实践”。我不指望能模拟板子正在交互的任何设备,因此可能必须在实际硬件上进行测试。
通过这样做,我希望能够获得驱动程序的单元测试覆盖数据,并促使开发人员编写测试以增加其驱动程序的覆盖范围。
我想到的一件事是编写嵌入式应用程序,在操作系统上运行并执行驱动程序代码,然后将结果传回测试工具。该设备有几个接口,我可以使用它们来驱动应用程序,以便可以对代码进行测试。
非常感谢任何其他建议或见解。
更新:虽然这可能不是精确的术语,但我所说的单元测试是指能够在不必编译整个操作系统+驱动程序并将其加载到设备上的情况下测试/运行代码。如果我必须这样做,我会称之为集成/系统测试。
问题在于我们拥有的硬件部件有限,而且通常由开发人员在修复错误等过程中使用。在此阶段将一个专用设备连接到CI服务器和自动化测试所在的机器上可能是不可取的。这就是为什么我正在寻找在不必实际构建整个驱动程序并将其上传到设备上的情况下测试驱动程序的方法的原因。

概述

根据下面的优秀答案,我认为一个合理的方法是使用IOCTL公开驱动程序功能,然后在嵌入式设备的应用程序空间中编写测试来实际执行驱动程序代码。

还有一种方法是在设备的应用程序空间中放置一个小型程序,该程序通过串行或USB公开API,以便可以在PC上编写主要的单元测试,并与硬件通信并运行测试。

如果项目刚刚开始,我认为我们会更加控制组件隔离的方式,以便大多数测试可以在PC级别完成。鉴于编码已经完成,我们正在尝试将测试工具和用例添加到系统中,我认为上述方法更加实际。

感谢大家的回答。

6个回答

9
在过去,我们测试和调试设备驱动程序的方式就是这样。对于这种系统进行调试的最佳方法是让工程师使用嵌入式系统作为开发系统,并在达到足够的系统成熟度后,拿走原始的交叉开发系统!
针对你的情况,有几种方法可以考虑:
- 添加ioctl处理程序:每个代码都执行特定的单元测试。 - 通过条件编译,在驱动程序中添加一个main()函数,对驱动程序中的功能单元进行测试,并将结果输出到stdout。 - 初步调试时,也许可以使其多平台可操作,这样就不必在目标硬件上进行调试。 - 也许条件代码还可以模拟一种环回式设备。

5
如果你的嵌入式系统足够大,可以在其中安装编译器,那么你不是在“古老的日子”里工作。 - Windows programmer

7
那些真正依赖于硬件的代码(分层架构中驱动程序堆栈的最底层)除了在硬件上或高质量的硬件模拟器上,其他地方都无法进行测试。
如果您的驱动程序具有某些更高级别的功能组件,而这些组件不直接依赖于硬件(例如,用于以特定格式向硬件发送消息的协议处理程序),并且该部分代码很好地自包含,则可以在基于PC的单元测试框架中单独对其进行单元测试。
回到最低层——如果它依赖于硬件,则测试夹具需要包括硬件。您可以制作一个包括硬件、驱动程序和一些测试软件的测试夹具。我认为主要的是将正常产品的应用代码从测试中排除,并代之以一些测试代码。测试代码可以系统地测试所有驱动程序的功能和边界情况(应用代码可能没有),并且还可以在短时间内强烈地测试驱动程序(应用程序可能不会)。因此,与仅运行应用程序相比,这是更有效地利用有限硬件的方法,并且可以获得更好的结果。
如果您能让PC参与其中,那么PC可能会帮助测试。例如,如果您正在为嵌入式设备编写串口驱动程序,则可以:
- 为嵌入式设备编写测试代码,发送各种已知数据流。 - 将其连接到PC的串口,运行验证传输数据流的测试代码。 - 在另一个方向上进行相同的操作——PC发送数据;嵌入式设备接收并验证它,并在有错误时通知PC。 - 测试可以以全速流式传输数据,并使用各种不同的字节时间(我曾经发现一个微控制器UART硅bug,只有在字节之间以约5毫秒的延迟发送字节时才会出现)。
您可以对以太网驱动程序、Wi-Fi驱动程序等进行类似的操作。
如果您正在测试存储设备驱动程序,例如EEPROM或Flash芯片的驱动程序,则PC无法以相同的方式参与。在这种情况下,您的测试工具可以测试各种写入条件(单字节、块...),并使用各种读取条件验证数据完整性。

4
我两三年前也遇到过类似的问题。我将一个设备驱动程序从VxWorks移植到Integrity。我们只改变了驱动程序的操作系统相关部分,但这是一个安全关键项目,所以所有单元测试和集成测试都需要重新进行。我们使用了一个名为LDRA testbed的自动化测试工具进行单元测试。我们的99%单元测试都在Windows机器上使用Microsoft编译器完成。现在我来解释一下如何做到这一点。
首先,当你进行单元测试时,你正在测试软件。当你将真实设备包含在测试中时,你还在测试设备。有时硬件或硬件文档可能存在问题。当你设计软件时,如果已经清楚地描述了每个函数的行为,那么进行单元测试就非常容易,例如考虑以下函数:
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函数)编写存根,并检查它是否使用正确的参数调用函数。如果您没有直接从驱动程序访问内存,则情况如此。您可以使用这种心态测试任何类型的驱动程序代码,而无需任何操作系统或设备。
如果您已将设备内存直接映射到内存空间中,并且设备驱动程序读取和写入设备内存,就像它自己的内存一样,那么情况会变得有些复杂。使用自动化测试工具,现在您必须观察指针的值,并根据这些指针的值定义通过/失败的标准。如果您正在从内存中读取值,则必须定义预期值。在某些情况下,这可能很困难。
还有一个问题,开发人员总是在驱动程序的单元测试中感到困惑,例如:
 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-2微秒内准备好,你就不需要担心这个问题。如果你的代码在这里失败了,你需要向设备制造商报告,而不是找到解决这个问题的方法。你明白我的意思吗?

希望这能帮到你……


谢谢您的评论。我本来想这样做,但是现在硬件相关和独立部分耦合度太高了,无法从板子上进行测试。 - Noufal Ibrahim

3

我两个月前就做过这个任务。

让我猜猜: 你可能有一些“代码片段”,可以向设备传递低级别的细节。你知道这些片段是有效的,但由于它们依赖于设备驱动程序,所以你无法对它们进行覆盖测试。

同样地,没有必要单独测试每一行代码。它们从未孤立运行,你的单元测试最后会变成生产代码的镜像反射。例如,如果你想启动设备,则需要创建一个连接、传递一个特定的低级别复位命令,然后是初始化参数结构等等。而如果你需要添加一些配置,则可能需要将其脱机、添加配置,然后再上线。像这样的东西。

你不希望测试低级别的东西。否则,你的单元测试只会反映你对设备工作方式的假设,而没有证实任何事情。

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

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

然后你可以分两个阶段测试: * 有一个简单的测试面板应用程序,作用于具体的适配器。它用于确认适配器实际上是有效的。例如,如果你按“开始”按钮,则它会启动。例如,如果你按照顺序按下“离线”、“传输(192)”和“在线”,则设备会如预期响应。这是你的集成测试。

你不需要对适配器中的细节进行单元测试。你手动测试它,因为唯一的成功标准是设备的响应。

然而,控制器完全经过单元测试。它只依赖于抽象,这在你的测试代码中被模拟出来。因此,你的代码没有依赖于设备驱动程序,因为没有涉及到具体的适配器。

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

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

适配器应该隐藏你只需要在设备更改时才需要更改的细节。

  1. 如果控制面板操作繁琐,需要不断重复很多次,或者需要特定设备的知识才能操作控制面板,那么你的粒度过高,应该将一些操作合并。测试面板应该是有意义的。

  2. 如果最终用户需求变化会影响适配器代码,那么你可能的粒度过低,应该将操作分开,以便在控制器类中进行测试驱动开发以适应需求变化。


2
我建议使用基于应用程序的测试。即使搭建脚手架可能很困难且成本高昂,但这里有很多收益:
  • 仅崩溃一次进程,而不是整个系统
  • 能够使用标准工具集(调试器、内存检查器等)
  • 克服硬件可用性限制
  • 更快的反馈:无需在设备上安装,只需编译和测试
  • ...
就命名而言,这可以称为组件测试。
应用程序可以以与目标操作系统相同的方式初始化设备驱动程序,也可以直接使用驱动程序的内部。前者更昂贵,但覆盖面更广。然后链接器将告知缺少哪些函数,并对它们进行桩化,可能使用 爆炸式桩

请纠正我,但这些是优势而不是“做事的方式”。我看到了这些优势,并且我自己也在推动它,但我需要一种不会耗费太多时间或金钱的方法来使其运作。 - Noufal Ibrahim

1

词汇

我不指望能模拟问题中涉及的任何设备,所以可能必须在实际硬件上测试它们。

那么,您打算退出单元测试。也许您可以使用以下其中一个表达方式:

  • 自动化测试:测试发生在没有用户输入的情况下(与手动测试相反)。
  • 集成测试:测试几个组件一起工作的情况(与单元测试相反)。
    在更大的范围内,如果您测试整个系统而不仅仅是几个组件,这被称为系统测试。

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

  • 组件测试:类似于集成测试或系统测试,但规模更小。
    注意:这三种组件-集成-系统测试共享一组不同规模的问题。相反,单元测试不会(参见下文)。

“真正”单元测试的优势

通过集成测试(或系统测试、组件测试),获得一些反馈,如测试覆盖率,确实很有趣。这是非常有用的。

但是,在某个点之后,要进一步取得进展非常困难(读作“非常昂贵”),因此我建议您使用补充方法,例如添加一些真正的单元测试。为什么?:

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

1
如果他正在孤立地测试单个驱动程序,即使他必须在硬件上进行测试,那么也许可以称之为单元测试。 - Craig McQueen
在宽泛的意义上,我想是这样。不过,我已经更新了问题,并表达了我的原意。 - Noufal Ibrahim
1
@Noufal 感谢您的更新。我现在明白了。我的回答的第二部分有用吗?否则,我可以删除我的回答,以消除一些噪音;-) - KLE
1
虽然这句话与确切的问题不是特别相关,但由于我正在构建我的测试框架并说服开发团队改变他们的工作方式,所以这样的评论非常有价值。 - Noufal Ibrahim

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