Non eseguire test di unità sul dispositivo o sull'emulatore Arduino
Il caso contro i test basati su dispositivo / emulatore / sim del microcontrollore
Ci sono molte discussioni su cosa significhi unit test e non sto davvero cercando di discutere su questo qui. Questo post non
ti dice di evitare tutti i test pratici sul tuo hardware di destinazione finale. Sto cercando di chiarire come ottimizzare il ciclo di feedback sullo sviluppo eliminando l'hardware di destinazione dai test più banali e frequenti. Si presume che le unità in prova siano molto più piccole dell'intero progetto.
Lo scopo del test unitario è testare la qualità del proprio codice. I test unitari in genere non devono mai testare la funzionalità di fattori al di fuori del proprio controllo.
Pensaci in questo modo: anche se dovessi testare la funzionalità della libreria Arduino, l'hardware del microcontrollore o un emulatore, è assolutamente impossibile che tali risultati dei test ti diano qualcosa sulla qualità del tuo lavoro. Pertanto, è molto più prezioso ed efficiente scrivere unit test che non vengono eseguiti sul dispositivo di destinazione (o sull'emulatore).
I test frequenti sull'hardware di destinazione hanno un ciclo dolorosamente lento:
- Modifica il tuo codice
- Compila e carica sul dispositivo Arduino
- Osserva il comportamento e indovina se il tuo codice sta facendo quello che ti aspetti
- Ripetere
Il passaggio 3 è particolarmente brutto se si prevede di ricevere messaggi diagnostici tramite la porta seriale ma il progetto stesso deve utilizzare l'unica porta seriale hardware del proprio Arduino. Se pensavi che la libreria SoftwareSerial potesse essere d'aiuto, dovresti sapere che è probabile che ciò interrompa qualsiasi funzionalità che richiede un tempismo accurato come la generazione di altri segnali allo stesso tempo. Questo problema mi è successo.
Ancora una volta, se dovessi testare il tuo schizzo usando un emulatore e le tue routine critiche per il tempo funzionassero perfettamente fino a quando non le avessi caricate sull'Arduino vero e proprio, l'unica lezione che imparerai è che l'emulatore è difettoso - e sapendolo ancora non rivela nulla sulla qualità del tuo lavoro.
Se è stupido test sul dispositivo o emulatore, che cosa dovrei fare?
Probabilmente stai usando un computer per lavorare al tuo progetto Arduino. Quel computer ha ordini di grandezza più veloci del microcontrollore. Scrivi i test per compilare ed eseguire sul tuo computer .
Ricorda, il comportamento della libreria Arduino e del microcontrollore dovrebbe essere considerato corretto o almeno costantemente errato .
Quando i tuoi test producono output in contrasto con le tue aspettative, allora probabilmente hai un difetto nel tuo codice che è stato testato. Se l'output del test corrisponde alle tue aspettative, ma il programma non si comporta correttamente quando lo carichi su Arduino, allora sai che i tuoi test erano basati su ipotesi errate e probabilmente hai un test errato. In entrambi i casi, ti verranno fornite informazioni reali su quali dovrebbero essere le tue successive modifiche al codice. La qualità del tuo feedback è migliorata da " qualcosa è rotto" a "questo codice specifico è rotto" .
Come compilare ed eseguire test sul PC
La prima cosa che devi fare è identificare i tuoi obiettivi di test . Pensa a quali parti del tuo codice vuoi testare, quindi assicurati di costruire il tuo programma in modo tale da poter isolare parti discrete per i test.
Se le parti che si desidera testare richiamano qualsiasi funzione Arduino, sarà necessario fornire sostituzioni del modello nel programma di test. Questo è molto meno lavoro di quanto sembri. I tuoi mock-up non devono fare altro che fornire input e output prevedibili per i tuoi test.
Qualsiasi tuo codice che intendi testare deve esistere in file di origine diversi dallo schizzo .pde. Non preoccuparti, il tuo schizzo verrà comunque compilato anche con un codice sorgente esterno allo schizzo. Quando si arriva davvero ad esso, nel file di schizzo dovrebbe essere definito poco più del normale punto di ingresso del programma.
Non resta che scrivere i test effettivi e quindi compilarlo utilizzando il compilatore C ++ preferito! Questo è probabilmente meglio illustrato con un esempio del mondo reale.
Un vero esempio funzionante
Uno dei miei progetti per animali domestici trovato qui ha alcuni semplici test eseguiti su PC. Per l'invio di questa risposta, esaminerò semplicemente come ho simulato alcune delle funzioni della libreria di Arduino e i test che ho scritto per testare quei modelli. Questo non è contrario a quello che ho detto prima di non testare il codice di altre persone perché sono stato io a scrivere i modelli. Volevo essere certo che i miei modelli fossero corretti.
Fonte di mock_arduino.cpp, che contiene codice che duplica alcune funzionalità di supporto fornite dalla libreria 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);
}
Uso il seguente mock-up per produrre output leggibili quando il mio codice scrive dati binari sul dispositivo seriale hardware.
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;
e, infine, l'attuale programma di test:
#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();
}
Questo post è abbastanza lungo, quindi fai riferimento al mio progetto su GitHub per vedere altri casi di test in azione. Tengo i miei lavori in corso in rami diversi da quelli master, quindi controlla anche quei rami per ulteriori test.
Ho scelto di scrivere le mie routine di test leggere, ma sono disponibili anche framework di unit test più robusti come CppUnit.