我如何单元测试Arduino代码?

Mat*_*och 167 embedded unit-testing avr arduino avr-gcc

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

正在开发Arduino仿真器可能很有用,但它似乎还没有准备好使用.

来自Atmel的AVR Studio包含一个可能有用的芯片模拟器,但我看不出如何将它与Arduino IDE结合使用.

Iro*_*ior 123

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

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

有很多关于单元测试意味着什么的讨论,我并不是真的想在这里讨论.这篇文章并未 告诉您避免对最终目标硬件进行所有实际测试.我试图通过从您最平凡和频繁的测试中消除目标硬件来优化您的开发反馈周期.假设被测单位比整个项目小得多.

单元测试的目的是测试您自己的代码的质量.单元测试通常不应测试控件之外的因素的功能.

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

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

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

如果您希望通过串行端口获取诊断消息,但步骤3特别令人讨厌,但您的项目本身需要使用Arduino唯一的硬件串行端口.如果您认为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);
}
Run Code Online (Sandbox Code Playgroud)

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

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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

最后,实际的测试程序:

#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();
}
Run Code Online (Sandbox Code Playgroud)

这篇文章很长,所以请参考我在GitHub上的项目,看看还有更多的测试用例.我将我的作品保留在除了主人以外的分支中,因此请检查这些分支以进行额外的测试.

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

  • @WarrenMacEvoy再一次,我认为你已经接受了我的建议,并把它变成了事实并非如此.你当然应该在它的实际环境中测试你的代码.我的论点是你不应该每天都这样做,你当然不应该把它称为单元测试. (4认同)

Mat*_*och 60

在没有任何预先存在的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();
}
Run Code Online (Sandbox Code Playgroud)

  • 测试似乎只在arduino上运行,因此您无法在开发计算机上自动执行它们.单元测试的基本思想是自动运行它们,因此当前的设计似乎更像是一个调试工具,但没有真正的单元测试框架. (17认同)
  • @MatthewMurdoch我担心你错了.根据定义,单元测试**永远不会在目标环境中运行.实际上,单元测试背后的想法是完全消除测试中的目标环境.它们总是在类似实验室的环境中运行,嘲弄被测单元外部的所有活动,以确保测试的成功或失败仅反映在被测单元上.这是人们在复杂项目中使用控制反转概念的最大原因之一. (10认同)
  • @ marcv81存在此类可移植性问题的区域很容易成为单元测试的对象。请记住,单元测试应仅测试您的代码,因此请相应地限制其范围。由于我们在这里谈论的硬件存在巨大差异,因此我可以接受某些情况是不可避免的。在这种情况下,工程师应保持认识并采取缓解措施。这可能意味着更改您的设计以提高可测试性,甚至只是记录相关事实就这么简单。 (2认同)
  • @Iron Savior单元测试可以测试您的代码,但是您的代码在某个地方运行。如果该上下文是或模拟Arduino上下文;然后ArdunoUnit将帮助您编写单元测试。如果您查看ArduinoUnit项目,则该框架的元测试会自动加载,运行并验证跨平台目标上的测试结果。就像您在其他跨平台目标上一样。您的观点是没有在嵌入式环境中测试代码的借口,在嵌入式环境中,正确性比其他情况要重要得多(即使不是很多)。 (2认同)

Dav*_*kes 21

通过抽象出硬件访问并在我的测试中模拟它,我在单元测试PIC代码方面取得了相当大的成功.

例如,我抽象PORTA

#define SetPortA(v) {PORTA = v;}
Run Code Online (Sandbox Code Playgroud)

然后可以轻松地模拟SetPortA,而无需在PIC版本中添加开销代码.

一旦硬件抽象测试了一段时间,我很快发现通常代码从测试装置到PIC并且第一次工作.

更新:

我使用#include接缝作为单元代码,#包括测试装备的C++文件中的单元代码,以及目标代码的C文件.

作为一个例子,我想复用四个7段显示器,一个端口驱动段,另一个端口选择显示器.显示代码通过SetSegmentData(char)和与显示器连接SetDisplay(char).我可以在我的C++测试装备中模拟这些并检查我是否得到了我期望的数据.对于我使用的目标#define,我得到一个直接赋值而没有函数调用的开销

#define SetSegmentData(x) {PORTA = x;}
Run Code Online (Sandbox Code Playgroud)


小智 15

似乎emulino完美地完成了这项工作.

Emulino是Greg Hewgill为Arduino平台设计的模拟器.(来源)

GitHub存储库


小智 11

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

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

在示例中有simduino,一个Arduino模拟器.它支持运行Arduino引导程序,可以通过Socat(一个经过修改的Netcat)进行avrdude编程.


pon*_*nty 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'
Run Code Online (Sandbox Code Playgroud)

开始测试:

$ nosetests pysimavr/examples/test_example.py
pysimavr.examples.test_example.test_atmega88 ... ok
Run Code Online (Sandbox Code Playgroud)


Yuv*_*dam 6

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

但是,还有Fritzing平台,您可以使用该平台对硬件进行建模,然后再导出PCB图表和内容.

值得一试.


tod*_*ish 6

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

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


小智 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脚本进行分析.
  • 该脚本启动下一个测试,对配置文件中请求的所有测试重复上述步骤.
  • 该脚本打印一个摘要,显示完整测试套件中所有失败/通过测试的概述.


Raf*_*ega 5

James W. Grenning撰写了很多很棒的书,这本书是关于嵌入式C的单元测试嵌入式C代码的测试驱动开发的


Ale*_*nze 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的人之一.


Ian*_*Ian 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;
};
Run Code Online (Sandbox Code Playgroud)

您可以按以下方式对其进行单元测试(使用称为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.
Run Code Online (Sandbox Code Playgroud)

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

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

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