我希望能够对我的Arduino代码进行单元测试。理想情况下,我希望能够运行任何测试而不必上传代码到Arduino。有哪些工具或库可以帮助我完成这个任务?
目前正在开发一款Arduino模拟器(Arduemu),这可能会有所帮助,但它似乎还没有准备好供使用。
Atmel的AVR Studio包含了一个芯片模拟器,这可能是有用的,但我不知道如何将其与Arduino IDE配合使用。
我希望能够对我的Arduino代码进行单元测试。理想情况下,我希望能够运行任何测试而不必上传代码到Arduino。有哪些工具或库可以帮助我完成这个任务?
目前正在开发一款Arduino模拟器(Arduemu),这可能会有所帮助,但它似乎还没有准备好供使用。
Atmel的AVR Studio包含了一个芯片模拟器,这可能是有用的,但我不知道如何将其与Arduino IDE配合使用。
有很多关于什么是单元测试的讨论,我并不是真的想在这里做出争论。本文不告诉你避免在最终目标硬件上进行所有实践测试。我的目的是通过从你最平凡和频繁的测试中消除目标硬件来优化你的开发反馈周期。被测试的单元假定比整个项目小得多。
单元测试的目的是测试你自己的代码质量。 单元测试通常不应测试你无法控制的因素的功能。
这样考虑:即使你要测试Arduino库、微控制器硬件或仿真器的功能,这些测试结果也绝对不可能告诉你任何关于你自己工作质量的信息。 因此,编写不在目标设备(或仿真器)上运行的单元测试要更有价值和高效。
在目标硬件上频繁测试具有极慢的周期:
如果您希望通过串口获得诊断信息,但您的项目本身需要使用您的Arduino唯一的硬件串口,则步骤3特别恶心。如果您认为SoftwareSerial库可能有帮助,则应知道这样做很可能会干扰任何需要准确计时的功能,例如同时生成其他信号。我曾经遇到过这个问题。
同样,如果您使用仿真器测试您的草图,直到实际上传到Arduino时,时间关键例程才运行完美,那么您所学到的唯一教训就是仿真器存在缺陷--即使这仍然不会揭示任何关于您自己工作质量的信息。
你可能正在使用电脑来开发你的Arduino项目。 该计算机比微控制器快几个数量级。 编写测试以构建和在你的计算机上运行。
请记住,Arduino库和微控制器的行为应被假定为正确或至少是一致的错误。
当你的测试输出与你的预期相反时,那么你很可能在被测试的代码中发现了缺陷。如果你的测试输出与你的预期相符,但是上传到Arduino后程序无法正常运行,那么你就知道你的测试基于错误的假设,你很可能有一个有缺陷的测试。无论哪种情况,你都会得到关于下一步代码更改应该是什么的真实见解。你的反馈质量从“有些东西出了问题”提高到了"这个具体的代码出了问题"。
第一件事是确定您的测试目标。想想您要测试自己的代码的哪些部分,然后确保以这样的方式构建程序,以便您可以隔离不同的部分进行测试。
如果您要测试的部分调用任何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。
由于没有现成的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;}
使用硬件抽象后,可以轻松地在不添加PIC版本中的额外代码的情况下对SetPortA进行模拟。
一旦硬件抽象得到测试,我很快发现通常代码从测试装置转移到PIC后第一次就能正常工作。
更新:
我在单元代码中使用#include接口,在C++文件的测试装置中包含单元代码,在目标代码的C文件中包含。例如:如果要复用四个7段显示器,其中一个端口驱动段,另一个端口选择显示器,则可通过SetSegmentData(char)
和SetDisplay(char)
与显示器代码进行接口交互。我可以在C++测试装置中模拟这些内容,并检查是否得到期望数据。对于目标代码,我使用#define
,以便获得直接赋值而无需调用函数的开销。
#define SetSegmentData(x) {PORTA = x;}
例如:
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_ci
。虽然它只能测试Arduino库(而不是独立的草图),但它可以在本地或CI系统(如Travis CI或Appveyor)上运行单元测试。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或从以下示例中复制: