我如何单元测试Arduino代码?

时间:2009-04-23 08:43:15

标签: unit-testing embedded arduino avr avr-gcc

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

有一个Arduino emulator in development可能有用,但它似乎还没有准备好使用。

来自Atmel的

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

20 个答案:

答案 0 :(得分:113)

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

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

  

关于单元测试的含义有很多讨论,我不是   真的想在这里争论一下。这篇文章是不是   告诉你要避免对你的最终目标进行所有实际测试   硬件。我试图说明如何优化你的   通过消除目标硬件来开发反馈周期   你最平凡和频繁的测试。假设被测单位   比整个项目小得多。

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

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

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

  1. 调整您的代码
  2. 编译并上传到Arduino设备
  3. 观察行为并猜测您的代码是否正在按预期执行
  4. 重复
  5. 如果您希望通过串口获取诊断消息,但步骤3特别令人讨厌,但您的项目本身需要使用您的Arduino唯一的硬件串口。如果您认为SoftwareSerial库可能有所帮助,您应该知道这样做可能会破坏任何需要精确计时的功能,例如同时生成其他信号。这个问题发生在我身上。

    同样,如果您要使用模拟器测试草图,并且您的时间关键例程运行良好,直到您上传到实际的Arduino,那么您将要学习的唯一教训是模拟器存在缺陷 - - 并且知道这仍然会发现没有关于你自己的工作的质量。

    如果在设备或模拟器上进行测试是愚蠢的,应该我做什么?

    您可能正在使用计算机来处理您的Arduino项目。该计算机的速度比微控制器快。编写测试以构建并在您的计算机上运行

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

    如果您的测试产生的输出与您的预期相反,那么您的代码中可能存在测试缺陷。如果您的测试输出符合您的期望,但是当您将程序上传到Arduino时程序运行不正常,那么您就知道您的测试基于错误的假设并且您可能存在缺陷测试。在任何一种情况下,您都将获得有关下一个代码更改应该是什么的真实见解。您的反馈质量会从" 某些被破坏后得到改善" "此特定代码已损坏"

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

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

    如果要测试的部件调用任何Arduino函数,则需要在测试程序中提供模拟替换。这比看起来要少得多。您的模型除了为您的测试提供可预测的输入和输出外,不必实际做任何事情。

    您打算测试的任何自己的代码都需要存在于.pde草图以外的源文件中。不用担心,即使在草图之外的某些源代码,您的草图仍然会编译。当你真正了解它时,只需要在草图文件中定义程序的正常入口点。

    剩下的就是编写实际的测试,然后使用您最喜欢的C ++编译器进行编译!这可能是一个真实世界的例子。

    实际工作示例

    我发现的一个宠物项目here有一些在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();
    }
    

    这篇文章足够长,所以请参考my project on GitHub以查看更多测试用例。我将我的作品保留在除主人以外的分支中,因此也要检查这些分支以进行额外的测试。

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

答案 1 :(得分: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();
}

答案 2 :(得分: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;}

答案 3 :(得分:15)

似乎emulino可以完美地完成这项工作。

  

Emulino是Greg Hewgill为Arduino平台设计的模拟器。 (Source

GitHub repository

答案 4 :(得分:11)

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

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

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

答案 5 :(得分:8)

您可以使用我的项目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 :(得分: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

答案 7 :(得分:6)

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

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

值得一试。

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

答案 9 :(得分:5)

将特定于硬件的代码与其他代码分开或抽象出来,这样您就可以在任何平台上测试和调试更大的“休息”,这些平台上有很好的工具,并且您最熟悉它。

基本上,尝试尽可能多地从已知的工作构建块构建最终代码。然后,剩余的硬件特定工作将变得更加容易和快捷。您可以通过自己使用现有的模拟器和/或模拟设备来完成它。然后,当然,你需要以某种方式测试真实的东西。根据具体情况,可能会或可能不会很好地自动化(即谁按或按什么按钮并提供其他输入?谁或什么会观察和解释各种指标和输出?)。

答案 10 :(得分: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的人之一。

答案 11 :(得分:4)

James W. Grenning撰写精彩的书籍,这是关于嵌入式C代码 Test Driven Development for Embedded C 的单元测试。

答案 12 :(得分:4)

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

考虑一个名为DoSomething的Arduino Library目录中一个非常简单的库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's ArduinoUnit library  他在his answer中提到过。

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

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

答案 13 :(得分:3)

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

来自项目描述

  

本机核心允许您编译和运行Arduino草图   PC,一般不做修改。它提供了原生版本   标准的Arduino函数,以及命令行的解释器   草图的输入通常来自硬件   本身。

同样在"what do I need to use it" section

  

如果你想构建测试,你需要cxxtest   http://cxxtest.tigris.org。 NCORE已经过cxxtest 3.10.1测试。

答案 14 :(得分:2)

如果您想在MCU外部(在桌面上)对代码进行单元测试,请查看libcheck: https://libcheck.github.io/check/

我用它来测试我自己的嵌入代码几次。这是非常强大的框架。

答案 15 :(得分:1)

尝试 Autodesk电路模拟器。它允许使用许多其他硬件组件测试Arduino代码和电路。

答案 16 :(得分:1)

将Proteus VSM与Arduino库一起使用来调试代码或进行测试。

这是将代码放到板上之前的最佳做法,但请确保时间安排,因为模拟不会在板上运行时实时运行。

答案 17 :(得分:1)

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

答案 18 :(得分:0)

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

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

-Nandha_Frost

答案 19 :(得分:0)

如果您有兴趣运行INO草图并检出串行输出,请在我的Arduino NMEA checksum项目中对此进行有效的实现。

以下脚本获取文件,并使用Arduino CLI将其编译为HEX文件,然后将其加载到SimAVR中,该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和十六进制,请运行:

./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退出,否则脚本将失败。