我希望能够对我的 Arduino 代码进行单元测试。理想情况下,我可以运行任何测试,而无需将代码上传到 Arduino。哪些工具或库可以帮助我解决这个问题?
开发中的Arduino 仿真器可能很有用,但似乎还没有准备好使用。
Atmel 的AVR Studio包含一个可能有用的芯片模拟器,但我不知道如何将它与 Arduino IDE 结合使用。
我希望能够对我的 Arduino 代码进行单元测试。理想情况下,我可以运行任何测试,而无需将代码上传到 Arduino。哪些工具或库可以帮助我解决这个问题?
开发中的Arduino 仿真器可能很有用,但似乎还没有准备好使用。
Atmel 的AVR Studio包含一个可能有用的芯片模拟器,但我不知道如何将它与 Arduino IDE 结合使用。
关于单元测试的含义有很多讨论,我并不是真的想在这里对此进行争论。这篇文章并不是 要您避免对最终目标硬件进行所有实际测试。我试图通过从最普通和最频繁的测试中消除您的目标硬件来优化您的开发反馈周期。假设被测单元比整个项目小得多。
单元测试的目的是测试你自己代码的质量。单元测试通常不应该测试您无法控制的因素的功能。
这样想:即使您要测试 Arduino 库、微控制器硬件或仿真器的功能,这些测试结果也绝对不可能告诉您有关您自己工作质量的任何信息。因此,编写不在目标设备(或模拟器)上运行的单元测试更有价值和更有效。
对目标硬件进行频繁测试的周期非常缓慢:
如果您希望通过串行端口获得诊断消息,但您的项目本身需要使用您的 Arduino 唯一的硬件串行端口,则第 3 步尤其令人讨厌。如果您认为 SoftwareSerial 库可能会有所帮助,您应该知道这样做可能会破坏任何需要准确时序的功能,例如同时生成其他信号。这个问题发生在我身上。
同样,如果您要使用仿真器测试您的草图,并且您的时间关键程序运行完美,直到您上传到实际的 Arduino,那么您要学习的唯一教训是仿真器有缺陷——并且仍然知道这一点没有透露你自己工作的质量。
您可能正在使用计算机来处理您的 Arduino 项目。那台计算机比微控制器快几个数量级。编写测试以在您的计算机上构建和运行。
请记住,应假定 Arduino 库和微控制器的行为正确或至少始终不正确。
当您的测试产生的输出与您的预期相反时,您的测试代码中可能存在缺陷。如果您的测试输出符合您的期望,但是当您将程序上传到 Arduino 时,它的行为不正确,那么您就知道您的测试是基于不正确的假设,并且您可能有一个有缺陷的测试。无论哪种情况,您都将真正了解下一次代码更改应该是什么。您的反馈质量从“某事已损坏”提高到“此特定代码已损坏”。
您需要做的第一件事是确定您的测试目标。考虑一下您想要测试您自己代码的哪些部分,然后确保以您可以隔离离散部分进行测试的方式构建您的程序。
如果您要测试的部件调用任何 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。
在没有任何预先存在的 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();
}
通过抽象出硬件访问并在我的测试中模拟它,我在单元测试我的 PIC 代码方面取得了相当大的成功。
例如,我将 PORTA 抽象为
#define SetPortA(v) {PORTA = v;}
然后可以轻松地模拟 SetPortA,而无需在 PIC 版本中添加开销代码。
一旦对硬件抽象进行了一段时间的测试,我很快就会发现通常代码会从测试台转到 PIC 并且第一次就可以工作。
更新:
我对单元代码使用#include seam,在测试装备的 C++ 文件中包含单元代码,为目标代码使用 C 文件。
例如,我想多路复用四个 7 段显示器,一个端口驱动段,第二个端口选择显示器。显示代码通过SetSegmentData(char)
和与显示接口SetDisplay(char)
。我可以在我的 C++ 测试台中模拟这些并检查我是否得到了我期望的数据。对于我使用的目标#define
,这样我就可以直接分配而没有函数调用的开销
#define SetSegmentData(x) {PORTA = x;}
您可以使用我的项目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
我们在大型科学实验中使用 Arduino 板进行数据采集。随后,我们必须支持几个具有不同实现的 Arduino 板。我编写了 Python 实用程序来在单元测试期间动态加载 Arduino 十六进制图像。下面链接中的代码通过配置文件支持 Windows 和 Mac OS X。要找出 Arduino IDE 放置十六进制图像的位置,请在点击构建(播放)按钮之前点击 shift 键。在点击上传的同时点击 shift 键以找出您的 avrdude(命令行上传实用程序)在您的系统/Arduino 版本上的位置。或者,您可以查看包含的配置文件并使用您的安装位置(当前在 Arduino 0020 上)。
该程序允许自动运行多个 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_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或仅从以下示例之一复制:
James W. Grenning 写了很多很棒的书,这本书是关于嵌入式 C 代码的单元测试嵌入式 C的测试驱动开发。
将特定于硬件的代码与其余部分分开或抽象出来,这样您就可以在任何您拥有良好工具且您最熟悉的平台上测试和调试更大的“其余部分”。
基本上,尝试从尽可能多的已知工作构建块构建尽可能多的最终代码。剩下的特定于硬件的工作将变得更容易和更快。您可以自己使用现有的模拟器和/或模拟设备来完成它。然后,当然,你需要以某种方式测试真实的东西。视情况而定,这可能会或可能不会很好地自动化(即谁或什么将按下按钮并提供其他输入?谁或什么将观察和解释各种指标和输出?)。
我在编写 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 的人之一。
有一个名为ncore的项目,它为 Arduino 提供原生内核。并允许您为 Arduino 代码编写测试。
从项目描述
本机内核允许您在 PC 上编译和运行 Arduino 草图,通常无需修改。它提供标准 Arduino 函数的本机版本,以及一个命令行解释器,用于为您的草图提供通常来自硬件本身的输入。
同样在“我需要使用什么”部分
如果你想构建测试,你需要来自 http://cxxtest.tigris.org的 cxxtest 。NCORE 已经使用 cxxtest 3.10.1 进行了测试。
如果您想在 MCU(桌面)之外对代码进行单元测试,请查看 libcheck: https ://libcheck.github.io/check/
我用它来测试我自己的嵌入式代码几次。这是一个非常强大的框架。
您可以使用emulare — 您可以将微控制器拖放到图表上并在 Eclipse 中运行您的代码。网站上的文档告诉您如何设置它。
试试Autodesk 电路模拟器。它允许使用许多其他硬件组件测试 Arduino 代码和电路。
使用带有 Arduino 库的 Proteus VSM 来调试您的代码或对其进行测试。
这是获取代码之前的最佳实践,但请确保时间安排,因为模拟在板上运行时不会实时运行。
基本的 Arduino 是用 C 和 C++ 编写的,甚至 arduino 的库也是用 C 和 C++ 编写的。因此,简单来说,只需将代码处理为 C 和 C++ 并尝试进行单元测试。在这里,“句柄”一词的意思是您将所有基本语法(例如 serial.println 更改为 sysout,pinmode 更改为变量,void 循环更改为 while() 循环,这会在 keystock 或经过一些迭代后中断。
我知道这是一个漫长的过程,而且不是那么直截了当。根据我的个人经验,一旦你开始处理它,这就会变得更可靠。
-Nandha_Frost
如果您有兴趣运行 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 退出,否则脚本将失败。