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

224

我希望能够对我的Arduino代码进行单元测试。理想情况下,我希望能够运行任何测试而不必上传代码到Arduino。有哪些工具或库可以帮助我完成这个任务?

目前正在开发一款Arduino模拟器(Arduemu),这可能会有所帮助,但它似乎还没有准备好供使用。

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


1
这个问题在2011年的Arduino论坛上有另一个帖子,链接为http://arduino.cc/forum/index.php?action=printpage;topic=54356.0。 - Jakob
1
感谢@Jakob。该线程中提到了一个Arduino模拟器(页面底部还有其他可能有用的链接):http://www.arduino.com.au/Simulator-for-Arduino.html - Matthew Murdoch
6
很遗憾,这只适用于Windows系统,我希望能找到一种在没有任何闭源或硬件依赖的情况下,通过命令行编译和运行Arduino代码的方法。 - Jakob
4
5年后的小更新:Simavr 仍然非常活跃,并且自提出该问题以来已经有了很大的改进,因此我认为它应该被推到更靠前的位置。它可能是回归测试、场景测试以及单元测试的合适工具。这样,你测试的代码就与目标硬件上的代码“相同”。 - zmo
1
现在有一个适用于此目的的GitHub操作,我是其作者。 - Ian
显示剩余2条评论
20个回答

166

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

反对基于微控制器设备/仿真器/模拟器进行测试的案例

有很多关于什么是单元测试的讨论,我并不是真的想在这里做出争论。本文告诉你避免在最终目标硬件上进行所有实践测试。我的目的是通过从你最平凡和频繁的测试中消除目标硬件来优化你的开发反馈周期。被测试的单元假定比整个项目小得多。

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

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

在目标硬件上频繁测试具有极慢的周期:

  1. 修改你的代码
  2. 编译并上传到Arduino设备
  3. 观察行为并猜测你自己的代码是否按预期执行
  4. 重复

如果您希望通过串口获得诊断信息,但您的项目本身需要使用您的Arduino唯一的硬件串口,则步骤3特别恶心。如果您认为SoftwareSerial库可能有帮助,则应知道这样做很可能会干扰任何需要准确计时的功能,例如同时生成其他信号。我曾经遇到过这个问题。

同样,如果您使用仿真器测试您的草图,直到实际上传到Arduino时,时间关键例程才运行完美,那么您所学到的唯一教训就是仿真器存在缺陷--即使这仍然不会揭示任何关于您自己工作质量的信息。

如果在设备或仿真器上进行测试很愚蠢,那么我该做些什么?

你可能正在使用电脑来开发你的Arduino项目。 该计算机比微控制器快几个数量级。 编写测试以构建和在你的计算机上运行

请记住,Arduino库和微控制器的行为应被假定为正确或至少是一致的错误

当你的测试输出与你的预期相反时,那么你很可能在被测试的代码中发现了缺陷。如果你的测试输出与你的预期相符,但是上传到Arduino后程序无法正常运行,那么你就知道你的测试基于错误的假设,你很可能有一个有缺陷的测试。无论哪种情况,你都会得到关于下一步代码更改应该是什么的真实见解。你的反馈质量从“有些东西出了问题”提高到了"这个具体的代码出了问题"

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

第一件事是确定您的测试目标。想想您要测试自己的代码的哪些部分,然后确保以这样的方式构建程序,以便您可以隔离不同的部分进行测试。

如果您要测试的部分调用任何Arduino函数,则需要在测试程序中提供模拟替代品。这比看起来要少得多。您的模拟不必实际执行任何操作,只需为测试提供可预测的输入和输出。

您打算测试的任何自己的代码都需要存在于除.pde sketch之外的源文件中。别担心,即使在草图之外有一些源代码,您的草图仍将编译。当你真正做到这一点时,草图文件中只应定义与程序正常入口点相关的内容。

现在剩下的就是撰写实际测试并使用您喜欢的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;

假冒串口.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项目,以查看更多的测试案例。我将正在进行的工作保留在其他分支中,因此请检查那些分支以获取额外的测试。

我选择编写自己的轻量级测试程序,但也有更健壮的单元测试框架,例如CppUnit。


1
这是一个很棒的答案!谢谢你! - Jonathan Arkell
7
@WarrenMacEvoy,我认为你已经把我的建议转化成了另外一件事情。你应该在某个时刻确保你的代码在实际环境中可以运行。我的观点是你不应该每天都这样做,而且你肯定不应该称其为单元测试。 - Iron Savior
1
@toasted_flakes,我不确定你从哪里得到那句话,但这不是我说的。在设备上运行的单元测试存在很多问题——反馈很慢,你可能没有任何串口或其他IO手段可以用于目标设备,而且它们的容量非常有限,这可能会影响你的测试套件的范围。 - Iron Savior
1
@ChristianHujer 你肯定应该在真实硬件上进行测试,没有人会说你永远不应该在目标硬件上进行测试。我的帖子是关于通过在开发机器上进行单元测试来缩短您的日常开发反馈循环时间。这种方式可以最小化您的测试开销,因为只有在必要时才会在目标硬件上进行测试。 - Iron Savior
1
@Benjohn Arduino的源代码文件曾经使用“pde”扩展名,尽管它们是C++。https://www.arduino.cc/en/Guide/Environment#toc1 - Iron Savior
显示剩余17条评论

69

由于没有现成的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();
}

21
测试似乎只能在Arduino上运行,所以不能在开发机器上自动执行。单元测试的基本思想是自动运行它们,因此当前的设计似乎更像是一个调试工具而不是真正的单元测试框架。 - Jakob
14
很抱歉,您的看法是错误的。按照定义,单元测试从不在目标环境中运行。实际上,单元测试的基本思想就是完全排除目标环境的影响。它们总是在类似实验室的环境中运行,模拟所有与被测试单元外部交互的活动,以确保测试的成功或失败仅反映在被测试的单元上。这也是人们在复杂项目中使用控制反转概念的最重要原因之一。 - Iron Savior
1
@IronSavior 我想这就是我的意思。如果你在开发环境而不是目标环境中运行单元测试,那么架构特定的代码怎么办?例如,如果一个测试依赖于整数类型的大小,在C++中未指定,但可能针对目标环境进行设置,并且在开发环境中可能会有所不同(因为你永远不知道谁会分叉你的项目)? - marcv81
2
@marcv81 存在可移植性问题的领域很可能不适合进行单元测试。请记住,单元测试应该只测试您的代码,因此应相应地限制其范围。考虑到我们在这里谈论的硬件差异非常大,我可以接受某些情况是不可避免的。在这种情况下,工程师应保持警觉并采取缓解措施。这可能意味着改变设计以提高可测试性,甚至只是记录相关事实。 - Iron Savior
2
@Iron Savior 单元测试可以测试您的代码,但是您的代码在某个地方运行。如果该上下文或模拟了Arduino上下文,则ArdunoUnit将帮助您编写单元测试。如果您查看ArduinoUnit项目,则框架的元测试会自动加载、运行和验证跨平台目标上的测试结果。就像您在其他跨平台目标上一样。您的观点是不测试嵌入式环境中的代码的借口,而在这种环境中,正确性与其他上下文一样重要,甚至更重要。 - Warren MacEvoy
显示剩余12条评论

23

通过将硬件访问的部分抽象出来并在我的测试中进行模拟,我已经成功地对我的 PIC 代码进行了单元测试。

例如,我使用以下代码来抽象 PORTA:

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

使用硬件抽象后,可以轻松地在不添加PIC版本中的额外代码的情况下对SetPortA进行模拟。

一旦硬件抽象得到测试,我很快发现通常代码从测试装置转移到PIC后第一次就能正常工作。

更新:

我在单元代码中使用#include接口,在C++文件的测试装置中包含单元代码,在目标代码的C文件中包含。例如:如果要复用四个7段显示器,其中一个端口驱动段,另一个端口选择显示器,则可通过SetSegmentData(char)SetDisplay(char)与显示器代码进行接口交互。我可以在C++测试装置中模拟这些内容,并检查是否得到期望数据。对于目标代码,我使用#define,以便获得直接赋值而无需调用函数的开销。

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

我原则上可以看到如何使用预处理器“seam”进行单元测试。然而,我不确定在没有模拟器来运行测试或者没有avr-gcc兼容的编译器(在我的情况下是输出Windows二进制文件)的情况下该如何做到这一点。 - Matthew Murdoch
谢谢更新。你是在PIC上还是在电脑上执行单元测试? - Matthew Murdoch
单元测试在 Mac 上使用 Xcode 运行。如果要在 Pic 上运行它们,可能需要某种仿真器。将其抽象化以在 Mac 上运行使得切换处理器变得更加容易。 - David Sykes
Arduino环境使用avr-gcc编译器,该编译器有一些特殊性质,这意味着使用gcc(或其他C++编译器)进行编译并在PC上运行可能并不意味着代码也可以在avr-gcc上编译。 - Matthew Murdoch
你在谈论什么样的差异?它们是那些不能用一些预处理指令来处理的东西吗? - Joseph Lisee
显示剩余2条评论

15

13

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

它已经支持一些ATTiny和ATMega微控制器,同时根据作者的说法,很容易添加更多支持。

在示例中,存在一款名为simduino的Arduino仿真器。它支持运行Arduino引导程序,并可以通过Socat(一个修改版的Netcat)来进行avrdude编程。


11
你可以使用我的项目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

7
我为此目的开发了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。这可能会引入由类型实现之间的冲突引起的错误。 - Cerin
这种类型的错误肯定是可能存在的。你有一个我可以用作测试案例的例子吗? - Ian

6
我们在一项大型科学实验中使用Arduino板进行数据采集。随后,我们需要支持具有不同实现的几个Arduino板。我编写了Python工具,在单元测试期间动态加载Arduino hex图像。下面链接中的代码通过配置文件支持Windows和Mac OS X。要找出Arduino IDE放置hex图像的位置,请在单击构建(播放)按钮之前按Shift键。按上传时按住Shift键以查找您的系统/ Arduino版本上avrdude(命令行上传实用程序)的位置。或者,您可以查看包含的配置文件并使用您的安装位置(目前在Arduino 0020上)。

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


+1 太棒了!您有任何关于在图像上传后如何进行单元测试的信息吗? - Matthew Murdoch
我们使用nosetests在Python端运行单元测试。每个测试的设置加载该测试的正确十六进制图像。我们从小规模测试开始,逐步扩展到更全面的测试。确保串行通信正常工作,确保串行集成到UI正常工作,并检查串行到DB的集成等等。analog_read_speed pde和py显示了这方面的基础知识(请参见上面的Github链接)。最终,我们将开放整个项目的源代码,请敬请关注。 :) - toddstavish

6
我不知道有哪个平台可以测试Arduino代码。
然而,有一个Fritzing平台,您可以使用它来模拟硬件,然后导出PCB图表和其他东西。
值得一试。

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 Unit Testing Library”编写:http://code.google.com/p/arduinounit 对于每组单元测试,执行以下步骤:
- 读取配置文件以找出要运行哪些测试。 - 脚本编译并上传包含单元测试代码的Arduino代码。 - 在Arduino板上运行单元测试。 - 测试结果通过串口打印并由Python脚本分析。 - 脚本启动下一个测试,重复以上步骤以执行所有在配置文件中请求的测试。 - 脚本打印摘要,显示完整测试套件中所有失败/通过测试的概览。

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