如何对Arduino代码进行单元测试?


185

我希望能够对Arduino代码进行单元测试。理想情况下,我将能够运行任何测试,而无需将代码上传到Arduino。哪些工具或库可以帮助我解决此问题?

开发中有一个Arduino仿真器可能很有用,但似乎尚未准备就绪。

Atmel的AVR Studio包含一个可能有用的芯片模拟器,但是我看不到如何将其与Arduino IDE结合使用。


从2011年开始,这个问题还有另一个线索,网址是arduino.cc/forum/index.php?action=printpage;topic=54356.0
Jakob

谢谢@雅各布。该线程中引用的Arduino模拟器(页面底部还有其他可能有用的链接):arduino.com.au/Simulator-for-Arduino.html
马修·默多克

5
不幸的是,它仅适用于Windows,我希望看到一种从命令行直接编译和运行Arduino代码的方法,而没有任何封闭的源代码或硬件依赖项。
雅各布2012年

3
5年后的一个小更新:Simavr仍然非常活跃,并且自提出问题以来已有很大改善,因此我认为值得将其推向更高的位置。对于回归测试,基于场景的测试以及为什么不是单元测试,它可能是正确的工具。这样,您测试的代码与目标硬件上的代码相同
zmo 2015年

对于重要项目,请考虑使用硬件测试仪。除了可以计时和测试按钮/开关反应,启动时间,温度,v / ma使用率,怪异的选项排列等之外,其他MCU也可以。是的,它要构建的硬件更多,但可以在修订版本中添加安全层。许多专业设备使用jtag等。
dandavis '16

Answers:


136

不要在Arduino设备或仿真器上运行单元测试

针对微控制器设备/仿真器/基于Sim的测试的案例

关于单元测试的含义有很多讨论,在这里我并不是真的想对此进行争论。这篇文章并没有 告诉您避免在最终目标硬件上进行所有实际测试。我试图通过从最普通和频繁的测试中消除目标硬件,来优化您的开发反馈周期。假定要测试的单元比整个项目小得多。

单元测试的目的是测试您自己的代码的质量。单元测试通常不应测试无法控制的因素的功能。

这样考虑:即使您要测试Arduino库,微控制器硬件或仿真器的功能,此类测试结果也绝对不可能告诉您任何有关您自己工作质量的信息。因此,编写不在目标设备(或仿真器)上运行的单元测试具有更大的价值和效率。

在目标硬件上进行频繁测试的周期非常缓慢:

  1. 调整您的代码
  2. 编译并上传到Arduino设备
  3. 观察行为并猜测您的代码是否正在按预期进行
  4. 重复

如果您希望通过串行端口获取诊断消息,但您的项目本身需要使用Arduino唯一的硬件串行端口,则第3步特别麻烦。如果您认为SoftwareSerial库可能会有所帮助,则应该知道这样做可能会破坏需要准确定时的任何功能,例如同时生成其他信号。这个问题发生在我身上。

同样,如果您要使用仿真器测试草图,并且时间紧迫的例程可以完美运行,直到将其上传到实际的Arduino,那么您将要学习的唯一一课就是仿真器存在缺陷-并且仍然需要了解这一点没有透露您自己的工作质量。

如果这是愚蠢的设备或模拟器上测试,什么应该怎么办?

您可能正在使用计算机来处理Arduino项目。那台计算机比微控制器快几个数量级。编写测试以在计算机上构建并运行

请记住,应该假设 Arduino库和微控制器的行为是正确的,或者至少始终是错误的

当您的测试产生的输出与您的期望相反时,则可能是您的测试代码存在缺陷。如果您的测试输出符合您的期望,但是将该程序上载到Arduino时该程序无法正常运行,那么您就知道您的测试基于错误的假设,并且您的测试可能有缺陷。无论哪种情况,您都将获得真正的见解,了解下一次代码更改应该是什么。您的反馈质量从“ 某些内容已损坏”提高到“此特定代码已损坏”

如何在PC上构建和运行测试

您需要做的第一件事就是确定您的测试目标。考虑一下要测试自己代码中的哪些部分,然后确保以可以隔离离散部分进行测试的方式构造程序。

如果要测试的部件调用了任何Arduino功能,则需要在测试程序中提供模型替代品。这比看起来要少得多的工作。除了为测试提供可预测的输入和输出之外,您的模型不需要执行任何操作。

您打算测试的任何您自己的代码都必须存在于.pde草图以外的源文件中。不用担心,即使草图外有一些源代码,您的草图仍然可以编译。当您真正了解它时,应该在草图文件中定义仅比程序的正常入口点多的东西。

剩下的就是编写实际的测试,然后使用您最喜欢的C ++编译器进行编译!最好用一个真实的例子来说明。

一个实际的工作例子

这里找到的我的一个宠物项目有一些在PC上运行的简单测试。对于此答案提交,我将介绍如何模拟Arduino库功能以及为测试这些模型而编写的测试。这与我之前所说的不测试其他人的代码并不矛盾,因为我是编写模型的人。我想非常确定我的模型是正确的。

mock_arduino.cpp的源代码,其中包含与Arduino库提供的某些支持功能重复的代码:

#include <sys/timeb.h>
#include "mock_arduino.h"

timeb t_start;
unsigned long millis() {
  timeb t_now;
  ftime(&t_now);
  return (t_now.time  - t_start.time) * 1000 + (t_now.millitm - t_start.millitm);
}

void delay( unsigned long ms ) {
  unsigned long start = millis();
  while(millis() - start < ms){}
}

void initialize_mock_arduino() {
  ftime(&t_start);
}

当我的代码将二进制数据写入硬件串行设备时,我使用以下模型来产生可读的输出。

fake_serial.h

#include <iostream>

class FakeSerial {
public:
  void begin(unsigned long);
  void end();
  size_t write(const unsigned char*, size_t);
};

extern FakeSerial Serial;

fake_serial.cpp

#include <cstring>
#include <iostream>
#include <iomanip>

#include "fake_serial.h"

void FakeSerial::begin(unsigned long speed) {
  return;
}

void FakeSerial::end() {
  return;
}

size_t FakeSerial::write( const unsigned char buf[], size_t size ) {
  using namespace std;
  ios_base::fmtflags oldFlags = cout.flags();
  streamsize oldPrec = cout.precision();
  char oldFill = cout.fill();

  cout << "Serial::write: ";
  cout << internal << setfill('0');

  for( unsigned int i = 0; i < size; i++ ){
    cout << setw(2) << hex << (unsigned int)buf[i] << " ";
  }
  cout << endl;

  cout.flags(oldFlags);
  cout.precision(oldPrec);
  cout.fill(oldFill);

  return size;
}

FakeSerial Serial;

最后是实际的测试程序:

#include "mock_arduino.h"

using namespace std;

void millis_test() {
  unsigned long start = millis();
  cout << "millis() test start: " << start << endl;
  while( millis() - start < 10000 ) {
    cout << millis() << endl;
    sleep(1);
  }
  unsigned long end = millis();
  cout << "End of test - duration: " << end - start << "ms" << endl;
}

void delay_test() {
  unsigned long start = millis();
  cout << "delay() test start: " << start << endl;
  while( millis() - start < 10000 ) {
    cout << millis() << endl;
    delay(250);
  }
  unsigned long end = millis();
  cout << "End of test - duration: " << end - start << "ms" << endl;
}

void run_tests() {
  millis_test();
  delay_test();
}

int main(int argc, char **argv){
  initialize_mock_arduino();
  run_tests();
}

这篇文章足够长,因此请参阅我在GitHub上的项目,以了解更多实际使用的测试案例。我将正在进行的工作放在master以外的分支中,因此也请检查这些分支以进行额外的测试。

我选择编写自己的轻量级测试例程,但也可以使用更强大的单元测试框架,例如CppUnit。


1
这是一个很好的答案!谢谢!
乔纳森·阿科尔

5
@WarrenMacEvoy再次,我认为您已经接受了我的建议,并将其转化为并非如此。您当然应该在SOME POINT的实际环境中测试您的代码。我的观点是,您不应每天都这样做,并且您当然不应该将其称为单元测试。
钢铁救世主

1
@toasted_flakes我不确定您在哪里得到该报价,但这不是我所说的。在设备上运行的单元测试存在很多问题-反馈环路非常慢,目标设备上可能没有任何串行端口或任何其他IO备用方式,并且它们的容量非常有限,可能会影响目标设备的范围您的测试套件。
钢铁救主

1
@ChristianHujer您当然应该在真实的硬件上进行测试-没人说您永远不要在目标硬件上进行测试。我的帖子是关于在开发机器上进行单元测试来加强您的日常开发反馈周期。这样可以最大程度地减少测试开销,因为仅在必要时才在目标硬件上进行测试。
钢铁救世主'18

1
@Benjohn Arduino素描源文件即使是C ++,也曾经具有“ pde”扩展名。arduino.cc/zh/Guide/Environment#toc1
Iron Savior

63

在没有任何针对Arduino的单元测试框架的情况下,我创建了ArduinoUnit。这是一个演示其用法的简单Arduino草图:

#include <ArduinoUnit.h>

// Create test suite
TestSuite suite;

void setup() {
    Serial.begin(9600);    
}

// Create a test called 'addition' in the test suite
test(addition) {
    assertEquals(3, 1 + 2);
}

void loop() {
    // Run test suite, printing results to the serial port
    suite.run();
}

18
这些测试似乎仅在arduino上运行,因此您无法在开发计算机上自动执行它们。单元测试的基本思想是自动运行它们,因此当前的设计似乎更多是一种调试工具,但没有真正的单元测试框架。
雅各布2012年

1
你是对的。为了能够在PC上运行它们,还需要Arduino或AVR仿真器。目前,Arduino库中没有真正的硬件抽象层,而当我看到它们时,AVR仿真器仍在开发中。如果现在事情发展了,那么原则上可以做到这一点。
马修·默多克

12
@MatthewMurdoch恐怕您不正确。根据定义,单元测试永远不会在目标环境中运行。实际上,单元测试背后的想法是从测试中完全消除目标环境。它们始终在类似于实验室的环境中运行,该环境模拟被测单元外部的所有活动,以确保测试的成功或失败仅反映在被测单元上。这是人们在复杂项目中使用“控制反转”概念的最大原因之一。
Iron Savior

2
@ marcv81存在此类可移植性问题的区域很容易成为单元测试的对象。请记住,单元测试应仅测试您的代码,因此请相应地限制其范围。鉴于我们在这里谈论的硬件存在巨大差异,我可以接受这样的情况可能是不可避免的。在这种情况下,工程师应保持认识并采取缓解措施。这可能意味着更改您的设计以提高可测试性,甚至只是记录相关事实就这么简单。
Iron Savior 2014年

2
@Iron Savior单元测试会测试您的代码,但是您的代码在某处运行。如果该上下文是或模拟Arduino上下文;然后ArdunoUnit将帮助您编写单元测试。如果您查看ArduinoUnit项目,则该框架的元测试会自动加载,运行并验证跨平台目标上的测试结果。就像您在其他跨平台目标上一样。您的观点是没有在嵌入式环境中测试代码的借口,在嵌入式环境中,正确性比其他情况要重要得多(即使不是很多)。
沃伦·麦克沃伊

21

通过提取硬件访问并在测试中模拟它,我在测试PIC代码方面取得了相当大的成功。

例如,我将PORTA与

#define SetPortA(v) {PORTA = v;}

这样就可以轻松模拟SetPortA,而无需在PIC版本中添加开销代码。

一旦对硬件抽象进行了一段时间的测试,我很快就会发现代码通常是从测试平台转到PIC,并且可以首次使用。

更新:

我将#include接缝用作单元代码,将#单元代码包含在用于测试装置的C ++文件中,并将C文件包含在目标代码中。

例如,我要多路复用四个7段显示器,一个端口驱动段,第二个端口选择显示器。显示代码通过SetSegmentData(char)和与显示器连接SetDisplay(char)。我可以在我的C ++测试装置中模拟这些,并检查是否获得了我期望的数据。对于目标,我使用#define它来获得直接分配而没有函数调用的开销

#define SetSegmentData(x) {PORTA = x;}

我原则上可以看到如何使用预处理器“ seam”进行单元测试。但是,我不确定在没有运行测试的仿真器或avr-gcc兼容的编译器输出Windows二进制文件的情况下如何做到这一点……
Matthew Murdoch

感谢更新。您是在PIC还是在PC上执行单元测试?
马修·默多克

单元测试在使用Xcode的Mac上运行。要在Pic上运行它们,可能需要某种仿真器。对其进行抽象处理使其在Mac上运行,使切换处理器变得更加容易
David Sykes

Arduino环境使用具有某些特质的avr-gcc编译器,这意味着使用gcc(或其他C ++编译器)进行编译并在PC上运行可能并不意味着代码也将在avr-gcc上进行编译。
马修·默多克

你在说什么区别?它们是某些预处理器指令无法处理的事情吗?
Joseph Lisee


12

simavr是使用avr-gcc 的AVR 模拟器

它已经支持一些ATTiny和ATMega微控制器,而且-根据作者所说,很容易添加更多。

示例中包含Arduino仿真器simduino。它支持运行Arduino引导加载程序,并且可以通过Socat(经过修改的Netcat)使用avrdude进行编程。


9

您可以使用我的项目PySimAVR在Python中进行单元测试。Arscons用于建筑和simavr用于仿真。

例:

from pysimavr.sim import ArduinoSim    
def test_atmega88():
    mcu = 'atmega88'
    snippet = 'Serial.print("hello");'

    output = ArduinoSim(snippet=snippet, mcu=mcu, timespan=0.01).get_serial()
    assert output == 'hello'

开始测试:

$ nosetests pysimavr/examples/test_example.py
pysimavr.examples.test_example.test_atmega88 ... ok

6

我不知道可以测试Arduino代码的任何平台。

但是,这里有Fritzing平台,您可以使用该平台对硬件进行建模,然后再用于导出PCB图表和其他内容。

值得检查。


6

我们正在使用Arduino开发板进行大型科学实验中的数据采集。随后,我们必须支持具有不同实现的多个Arduino板。我编写了Python实用程序来在单元测试期间动态加载Arduino十六进制图像。在下面的链接上找到的代码通过配置文件支持Windows和Mac OSX。要找出Arduino IDE放置的十六进制图像的位置,请在按下构建(播放)按钮之前按Shift键。按下Shift键的同时点击上载,以查找您的avrdude(命令行上载实用程序)在您的Arduino系统/版本上的位置。另外,您可以查看随附的配置文件并使用您的安装位置(当前在Arduino 0020上)。

http://github.com/toddstavish/Python-Arduino-Unit-Testing


+1好东西!上传图片后,您是否有关于单元测试的信息?
马修·默多克

我们使用过鼻子测试在python端运行单元测试。每个测试的设置都会加载该测试的正确十六进制图像。我们从小处着手,然后进行更全面的测试。确保串行通信正常工作,确保与UI的串行集成正常工作,检查与DB的串行集成,等等。最终,我们将开源整个项目,敬请期待。:)
toddstavish

6

该程序允许自动运行多个Arduino单元测试。测试过程在PC上开始,但是测试在实际的Arduino硬件上运行。通常使用一组单元测试来测试一个Arduino库。(这个

Arduino论坛:http//arduino.cc/forum/index.php? topic = 140027.0

GitHub项目页面:http : //jeroendoggen.github.com/Arduino-TestSuite

Python软件包索引中的页面:http : //pypi.python.org/pypi/arduino_testsuite

单元测试使用“ Arduino单元测试库”编写:http : //code.google.com/p/arduinounit

对每组单元测试执行以下步骤:

  • 阅读配置文件以找出要运行的测试
  • 该脚本编译并上传包含单元测试代码的Arduino草图。
  • 单元测试在Arduino板上运行。
  • 测试结果打印在串行端口上,并通过Python脚本进行分析。
  • 该脚本开始下一个测试,对配置文件中请求的所有测试重复上述步骤。
  • 该脚本将打印摘要,以显示整个测试套件中所有失败/通过的测试的概述。

5

使特定于硬件的代码与其余代码分开或抽象,以便您可以在拥有良好工具并且最熟悉的任何平台上测试和调试更大的“其余部分”。

基本上,尝试从尽可能多的已知工作模块中构建尽可能多的最终代码。剩下的特定于硬件的工作将变得更加容易和快捷。您可以通过使用现有的模拟器和/或自行模拟设备来完成此操作。然后,当然,您需要以某种方式测试真实的事物。根据情况的不同,它的自动化程度可能不佳(即,谁或什么将按下按钮并提供其他输入?谁或什么将观察并解释各种指示符和输出?)。



5

我在编写Arduino代码时使用Searduino。Searduino是Arduino模拟器和开发环境(Makefiles,C代码...),使用您喜欢的编辑器可以很容易地用C / C ++进行黑客入侵。您可以导入Arduino草图并在模拟器中运行它们。

Searduino 0.8的屏幕截图:http://searduino.files.wordpress.com/2014/01/jearduino-0-8.png

Searduino 0.9将在最后测试完成后的一两天内发布,并会录制视频。

在模拟器上进行的测试不应视为真正的测试,但是它无疑对我发现愚蠢/逻辑错误(忘记做pinMode(xx, OUTPUT)等)有很大帮助。

顺便说一句:我是开发Searduino的人之一。


5

arduino_ci为此目的而建造。尽管它仅限于测试Arduino库(而不是独立的草图),但它使单元测试可以在本地或在CI系统(例如Travis CI或Appveyor)上运行。

考虑一下您的Arduino库目录中一个非常简单的库,名为DoSomething,其内容为do-something.cpp

#include <Arduino.h>
#include "do-something.h"

int doSomething(void) {
  return 4;
};

您将按以下方式对其进行单元测试(使用一个名为test/is_four.cpp或此类测试文件):

#include <ArduinoUnitTests.h>
#include "../do-something.h"

unittest(library_does_something)
{
  assertEqual(4, doSomething());
}

unittest_main()  // this is a macro for main().  just go with it.

就这样。如果该assertEqual语法和测试结构看起来很熟悉,那是因为我采用了 他在回答中提到的Matthew Murdoch的ArduinoUnit库

有关单元测试I / O引脚,时钟,串行端口等的更多信息,请参见Reference.md

这些单元测试是使用ruby gem中包含的脚本进行编译和运行的。有关如何进行设置的示例,请参见README.md或仅从以下示例之一进行复制:


这看起来很有趣,但是我不确定它是否正在正确测试Arduino代码。从您发布的输出中,它可以编译为x86_64体系结构,该体系结构显然没有用于Arduino。这可能会引入由类型实现之间的冲突引起的错误。
塞林

这种错误肯定是可能的。您是否有我可以用于测试用例的示例?
伊恩

3

有一个名为ncore的项目,该项目为Arduino提供了本机内核。并允许您为Arduino代码编写测试。

从项目描述

本机内核允许您在PC上编译和运行Arduino草图,通常无需进行任何修改。它提供了标准Arduino功能的本机版本,以及一个命令行解释器,可为您的草图提供通常来自硬件本身的输入。

也在“我需要使用它”部分上

如果要构建测试,则需要http://cxxtest.tigris.org中的 cxxtest 。NCORE已通过cxxtest 3.10.1测试。


这是一个有趣的项目。不幸的是,它看起来已经死了,因为它已经6年没有进展了。
塞林

2

如果要在MCU外部(在台式机上)对代码进行单元测试,请检查libcheck: https

我用它几次测试了自己的嵌入式代码。这是一个非常健壮的框架。


唯一的缺点是它不支持g ++,这使得它对于测试大多数使用C ++功能的Arduino库毫无用处。
塞林

1

您可以使用emulare-您可以将微控制器拖放到图上并在Eclipse中运行代码。网站上的文档告诉您如何进行设置。


1

结合使用Proteus VSM和Arduino库来调试或测试代码。

这是在将代码带入板上之前的最佳做法,但是请确保使用时序,因为仿真不会像在板上一样实时运行。



0

基本的Arduino是用C和C ++编写的,甚至arduino的库也是用C和C ++编写的。因此,简单来说,只需将代码作为C和C ++进行处理,然后尝试进行单元测试即可。在这里,“ handle”一词的意思是您将所有基本语法(如serial.println)更改为sysout,将pinmode更改为变量,将void循环更改为while()循环,该循环在密钥库中或在某些迭代后会中断。

我知道这是一个漫长的过程,并不是那么简单。根据我的个人经验,一旦您开始使用它,它将变得更加可靠。

-Nandha_Frost


0

如果您有兴趣运行INO草图并检出串行输出,我可以在Arduino NMEA校验和中实现该工作的实现项目。

以下脚本获取文件,并使用Arduino CLI将其编译为HEX文件,然后将其加载到SimAVR中,该文件将对其进行评估并打印串行输出。由于所有Arduino程序都会永久运行,而实际上没有杀死自己的选项(exit(0)不起作用),因此我让草图运行了几秒钟,然后将捕获的输出与期望的输出进行比较。

下载并解压缩Arduino CLI(在本例中为0.5.0版-在撰写本文时为最新版本):

curl -L https://github.com/arduino/arduino-cli/releases/download/0.5.0/arduino-cli_0.5.0_Linux_64bit.tar.gz -o arduino-cli.tar.gz
tar -xvzf arduino-cli.tar.gz

现在,您可以更新索引并安装适当的核心:

./arduino-cli core update-index
./arduino-cli core install arduino:avr

假设您的草图名为nmea-checksum.ino,要获取ELF和HEX,请运行:

./arduino-cli compile -b arduino:avr:uno nmea-checksum.ino

接下来,SimAVR运行HEX(或ELF)-我从源代码构建,因为最新版本对我不起作用:

sudo apt-get update
sudo apt-get install -y build-essential libelf-dev avr-libc gcc-avr freeglut3-dev libncurses5-dev pkg-config
git clone https://github.com/buserror/simavr.git
cd simavr
make

成功的编译将为您提供simavr/run_avr用于运行草图的工具。就像我说的,timeout否则它将永远不会终止:

cd simavr
timeout 10 ./run_avr -m atmega168 -f 16000000 ../../nmea-checksum.ino.arduino.avr.uno.elf &> nmea-checksum.ino.clog || true

生成的文件将包含用于包装串行输出的ANSI颜色代码控制字符,以消除这些字符:

cat nmea-checksum.ino.clog | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g" > nmea-checksum.ino.log
cat nmea-checksum.ino.log

现在,您需要做的就是将此文件与一个已知的好文件进行比较:

diff nmea-checksum.ino.log ../../nmea-checksum.ino.test

如果没有差异,diff将以代码0退出,否则脚本将失败。

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.