我希望能够对Arduino代码进行单元测试。理想情况下,我将能够运行任何测试,而无需将代码上传到Arduino。哪些工具或库可以帮助我解决此问题?
开发中有一个Arduino仿真器可能很有用,但似乎尚未准备就绪。
Atmel的AVR Studio包含一个可能有用的芯片模拟器,但是我看不到如何将其与Arduino IDE结合使用。
我希望能够对Arduino代码进行单元测试。理想情况下,我将能够运行任何测试,而无需将代码上传到Arduino。哪些工具或库可以帮助我解决此问题?
开发中有一个Arduino仿真器可能很有用,但似乎尚未准备就绪。
Atmel的AVR Studio包含一个可能有用的芯片模拟器,但是我看不到如何将其与Arduino IDE结合使用。
Answers:
关于单元测试的含义有很多讨论,在这里我并不是真的想对此进行争论。这篇文章并没有 告诉您避免在最终目标硬件上进行所有实际测试。我试图通过从最普通和频繁的测试中消除目标硬件,来优化您的开发反馈周期。假定要测试的单元比整个项目小得多。
单元测试的目的是测试您自己的代码的质量。单元测试通常不应测试无法控制的因素的功能。
这样考虑:即使您要测试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接缝用作单元代码,将#单元代码包含在用于测试装置的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 OSX。要找出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
对每组单元测试执行以下步骤:
使特定于硬件的代码与其余代码分开或抽象,以便您可以在拥有良好工具并且最熟悉的任何平台上测试和调试更大的“其余部分”。
基本上,尝试从尽可能多的已知工作模块中构建尽可能多的最终代码。剩下的特定于硬件的工作将变得更加容易和快捷。您可以通过使用现有的模拟器和/或自行模拟设备来完成此操作。然后,当然,您需要以某种方式测试真实的事物。根据情况的不同,它的自动化程度可能不佳(即,谁或什么将按下按钮并提供其他输入?谁或什么将观察并解释各种指示符和输出?)。
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的人之一。
我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或仅从以下示例之一进行复制:
有一个名为ncore的项目,该项目为Arduino提供了本机内核。并允许您为Arduino代码编写测试。
从项目描述
本机内核允许您在PC上编译和运行Arduino草图,通常无需进行任何修改。它提供了标准Arduino功能的本机版本,以及一个命令行解释器,可为您的草图提供通常来自硬件本身的输入。
如果要构建测试,则需要http://cxxtest.tigris.org中的 cxxtest 。NCORE已通过cxxtest 3.10.1测试。
尝试使用Autodesk电路模拟器。它允许使用许多其他硬件组件测试Arduino代码和电路。
如果您有兴趣运行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退出,否则脚本将失败。