37 Stimmen

Unit-Tests für Gerätetreiber

Ich habe eine Situation, in der ich Folgendes schreiben muss algunos Unit-Tests für einige Gerätetreiber für eingebettete Hardware. Der Code ist ziemlich alt und groß und hat leider nicht viele Tests. Im Moment ist die einzige Art von Tests, die möglich ist, das Betriebssystem vollständig zu kompilieren, es auf das Gerät zu laden, es in realen Szenarien zu verwenden und zu sagen, dass es funktioniert. Es gibt keine Möglichkeit, einzelne Komponenten zu testen.

Ich bin auf eine schöner Thread hier, der Unit-Tests für eingebettete Geräte diskutiert von dem ich viele Informationen erhalten habe. Ich möchte ein wenig spezifischer werden und fragen, ob jemand irgendwelche "Best Practices" für das Testen von Gerätetreibern in einem solchen Szenario kennt. Ich erwarte nicht, dass ich in der Lage sein werde, eines der Geräte zu simulieren, mit denen die fragliche Karte kommuniziert, und werde sie daher wahrscheinlich auf der tatsächlichen Hardware selbst testen müssen.

Auf diese Weise hoffe ich, Daten zur Testabdeckung für die Treiber zu erhalten und die Entwickler dazu zu bewegen, Tests zu schreiben, um die Abdeckung ihrer Treiber zu erhöhen.

Eine Möglichkeit, die mir einfällt, ist das Schreiben von eingebetteten Anwendungen, die auf dem Betriebssystem laufen und den Treibercode trainieren und dann die Ergebnisse zurück an das Test-Harness übermitteln. Das Gerät verfügt über eine Reihe von Schnittstellen, über die ich die Anwendung wahrscheinlich von meinem Test-PC aus steuern kann, um den Code zu üben.

Für weitere Vorschläge oder Erkenntnisse wären wir sehr dankbar.


Update: Auch wenn es vielleicht nicht die exakte Terminologie ist, meinte ich mit "Unit Testing" die Möglichkeit, Code zu testen/zu üben, ohne das gesamte Betriebssystem und die Treiber kompilieren und auf das Gerät laden zu müssen. Wenn ich das tun müsste, würde ich es Integrations-/Systemtests nennen.

Das Problem ist, dass die Hardware, die wir haben, begrenzt ist und oft von den Entwicklern benutzt wird, um Fehler zu beheben usw. Einen dedizierten Rechner zu haben, der mit dem Rechner verbunden ist, auf dem der CI-Server und die automatisierten Tests durchgeführt werden, könnte in diesem Stadium nicht in Frage kommen. Deshalb suche ich nach Möglichkeiten, den Treiber zu testen, ohne das ganze Ding tatsächlich zu bauen und auf das Gerät zu laden.


Zusammenfassung

Basierend auf den ausgezeichneten Antworten unten, denke ich, dass ein vernünftiger Weg, um das Problem zu nähern wäre, um Treiber-Funktionalität mit IOCTLs und dann schreiben Tests in den Anwendungsbereich des eingebetteten Geräts, um tatsächlich den Treiber-Code ausüben.

Es wäre auch sinnvoll, ein kleines Programm im Anwendungsbereich des Geräts zu haben, das eine API zur Verfügung stellt, die den Treiber über die serielle Schnittstelle oder den USB-Anschluss ansprechen kann, so dass der Kern des Einheitstests auf einem PC geschrieben werden kann, der mit der Hardware kommuniziert und den Test ausführt.

Wenn das Projekt erst am Anfang stünde, hätten wir meiner Meinung nach mehr Kontrolle über die Art und Weise, wie die Komponenten isoliert werden, so dass die Tests hauptsächlich auf PC-Ebene durchgeführt werden können. In Anbetracht der Tatsache, dass die Codierung bereits abgeschlossen ist und wir versuchen, das Testsystem und die Testfälle nachträglich in das System einzubauen, halte ich den oben beschriebenen Ansatz für praktischer.

Ich danke Ihnen allen für Ihre Antworten.

9voto

wallyk Punkte 55322

Früher haben wir auf diese Weise Gerätetreiber getestet und debuggt. Der beste Weg, ein solches System zu debuggen, bestand darin, dass die Ingenieure das eingebettete System als Entwicklungssystem nutzten und - sobald eine ausreichende Systemreife erreicht war - das ursprüngliche Cross-Development-System entfernten!

Für Ihre Situation bieten sich mehrere Ansätze an:

  • Hinzufügen von ioctl-Handlern: jeder Code führt einen bestimmten Einheitstest durch
  • Fügen Sie bei der bedingten Kompilierung dem Treiber eine main() hinzu, die funktionale Einheitstests im Treiber durchführt und die Ergebnisse an stdout .
  • Um die Fehlersuche zu erleichtern, könnte man dies vielleicht plattformübergreifend machen, so dass man nicht auf der Zielhardware debuggen muss.
  • Vielleicht kann bedingter Code auch ein Loopback-Gerät emulieren.

7voto

Craig McQueen Punkte 39286

Der Code, der wirklich von der Hardware abhängig ist (die niedrigstes Niveau des Treiberstapels in einer mehrschichtigen Architektur) können nur auf der Hardware oder einer hochwertigen Simulation der Hardware getestet werden.

Wenn Ihr Treiber eine Komponente von übergeordnete Ebene Funktionalität, die nicht direkt mit der Hardware zusammenhängt (z.B. ein Protokoll-Handler für das Senden von Nachrichten an die Hardware in einem bestimmten Format), und wenn dieser Teil gut in den Code eingebettet ist, dann können Sie ihn separat in einem PC-basierten Unit-Test-Framework testen.

Um auf die unterste Ebene zurückzukommen - wenn es von der Hardware abhängt, dann muss die Prüfvorrichtung die Hardware enthalten. Sie können eine Testvorrichtung bauen, die die Hardware, den Treiber und etwas Testsoftware enthält. Das Wichtigste ist meiner Meinung nach, den Anwendungscode des normalen Produkts aus dem Test herauszunehmen und stattdessen einen Testcode einzubauen. Der Testcode kann systematisch alle Funktionen und Eckfälle des Treibers testen (was der Anwendungscode möglicherweise nicht kann), und er kann auch den Treiber in kurzer Zeit intensiv testen (was die Anwendung wahrscheinlich nicht kann). Dadurch wird die begrenzte Hardware effizienter genutzt als durch das Ausführen der Anwendung, und Sie erhalten bessere Ergebnisse.

Wenn Sie einen PC in die Schleife einbinden können, könnte der PC bei den Tests helfen. Wenn Sie z. B. einen Treiber für eine serielle Schnittstelle für ein eingebettetes Gerät schreiben, können Sie das tun:

  • Schreiben Sie Testcode für das eingebettete Gerät, das verschiedene bekannte Datenströme sendet.
  • Schließen Sie das Gerät an die serielle Schnittstelle eines PCs an und führen Sie einen Testcode aus, der die übertragenen Datenströme prüft.
  • Das Gleiche gilt in umgekehrter Richtung: Der PC sendet Daten, das eingebettete Gerät empfängt sie, überprüft sie und benachrichtigt den PC über etwaige Fehler.
  • Die Tests können Daten mit voller Geschwindigkeit übertragen und mit verschiedenen Byte-Timings spielen (ich habe einmal einen Mikrocontroller-UART-Silikonfehler gefunden, der nur auftrat, wenn Bytes mit einer Verzögerung von ~5 ms zwischen den Bytes gesendet wurden).

Ähnlich könnte man mit einem Ethernet-Treiber oder einem Wi-Fi-Treiber verfahren.

Wenn Sie einen Treiber für ein Speichergerät testen, z. B. für einen EEPROM- oder Flash-Chip, kann der PC nicht auf die gleiche Weise beteiligt sein. In diesem Fall könnte Ihr Test-Kabelbaum alle Arten von Schreibbedingungen (Einzelbyte, Block...) testen und die Datenintegrität unter Verwendung aller Arten von Lesebedingungen überprüfen.

4voto

Caner Altinbasak Punkte 103

Ich hatte vor zwei oder drei Jahren ein ähnliches Problem. Ich habe einen Gerätetreiber von VxWorks nach Integrity portiert. Wir hatten nur betriebssystemabhängige Teile des Treibers geändert, aber es handelte sich um ein sicherheitskritisches Projekt, so dass alle Einheitstests und Integrationstests neu erstellt wurden. Wir haben für unsere Unit-Tests ein automatisiertes Testwerkzeug namens LDRA Testbed verwendet. 99 % unserer Unit-Tests werden auf Windows-Maschinen mit Microsoft-Compilern durchgeführt. Jetzt erkläre ich, wie man das macht

Nun, zunächst einmal testen Sie beim Unit-Testing eine Software. Wenn Sie das echte Gerät in Ihre Tests einbeziehen, testen Sie auch das Gerät. Manchmal kann es Probleme mit der Hardware oder der Dokumentation der Hardware geben. Wenn Sie die Software entwerfen und das Verhalten jeder Funktion klar beschrieben haben, ist es sehr einfach, Unit-Tests durchzuführen, zum Beispiel: Denken Sie an die Funktion;

readMessageTime(int messageNo, int* time); 
//This function calculates the message location, if the location is valid, 
//it reads    the time information 
address=calculateMessageAddr(messageNo); 
if(address!=NULL) { 
    read(address+TIME_OFFSET,time); 
    return success; 
} 
else { 
return failure; 
} 

Nun, hier testen Sie nur, ob readMessageTime das tut, was es tun soll. Sie brauchen nicht zu testen, ob calculateMessageAddr das richtige Ergebnis berechnet oder read die richtige Adresse liest. Dafür sind andere Unit-Tests zuständig. Was Sie also tun müssen, ist Stubs für calculateMessageAddr und read(OS function) zu schreiben und zu prüfen, ob die Funktionen mit den richtigen Parametern aufgerufen werden. Dies ist der Fall, wenn Sie nicht direkt von Ihrem Treiber auf den Speicher zugreifen. Sie können jede Art von Treibercode ohne Betriebssystem oder Gerät mit dieser Mentalität testen.

Wenn Sie den Gerätespeicher direkt in Ihren Speicherbereich gemappt haben und der Gerätetreiber den Gerätespeicher liest und schreibt, als wäre es sein eigener Speicher, wird es etwas kompliziert. Bei der Verwendung von automatisierten Testwerkzeugen müssen Sie nun die Werte von Zeigern überwachen und Pass/Fall-Kriterien entsprechend den Werten dieser Zeiger definieren. Wenn Sie einen Wert aus dem Speicher lesen, müssen Sie den erwarteten Wert definieren. Dies kann in einigen Fällen schwierig sein.

Es gibt noch ein weiteres Problem, das Entwickler bei Unit-Tests von Treibern wie diesem immer wieder verwirrt:

 readMessageTime(int messageNo, int* time); 
 //This function calculates the message location, if the location is valid,
 //it does some jobs to make the device ready to read then 
 //it reads the time information 
 address=calculateMessageAddr(messageNo); 
 if(address!=NULL) { 
      do_smoething(); // Get the device ready to read!    
      do_something_else() // do some other stuff so you can read the result in 3us.
      status=NOT_READY;
      while(status==NOT_READY) // mustn't be longer than 3us.
           status=read(address+TIME_OFFSET,time); 
      return success; 
  } else 
  { 
  return failure; 
  } 

Hier erledigen do_something und do_something_else einige Aufgaben auf dem Gerät, um es zum Lesen bereit zu machen. Entwickler fragen sich immer: "Was ist, wenn das Gerät für immer nicht bereit ist und mein Code hier in eine Sackgasse gerät", und sie neigen dazu, diese Art von Dingen auf dem Gerät zu testen.

Nun, man muss dem Gerätehersteller und dem technischen Autor vertrauen. Wenn sie sagen, dass das Gerät in 1 bis 2 Stunden fertig sein wird, brauchen Sie sich keine Sorgen zu machen. Wenn Ihr Code hier fehlschlägt, müssen Sie dies dem Gerätehersteller melden, es ist nicht Ihre Aufgabe, eine Umgehung zu finden, um dieses Problem zu umgehen. Haben Sie meinen Standpunkt verstanden?

Ich hoffe, das hilft .

3voto

Tormod Punkte 4245

Genau diese Aufgabe hatte ich erst vor zwei Monaten.

Lassen Sie mich raten: Sie haben wahrscheinlich "Codeschnipsel", die dem Gerät Details auf niedriger Ebene mitteilen. Sie wissen, dass diese Schnipsel funktionieren, aber Sie können sie nicht abdecken, weil sie von den Gerätetreibern abhängig sind.

Ebenso wenig ist es sinnvoll, jede einzelne Zeile einzeln zu prüfen. Sie werden nie isoliert ausgeführt, und Ihr Unit-Test würde am Ende wie ein Spiegelbild des Produktionscodes aussehen. Wenn Sie z. B. das Gerät starten wollen, müssen Sie eine Verbindung herstellen, ihm einen bestimmten Reset-Befehl auf niedriger Ebene übergeben, dann eine Initialisierungsparameterstruktur usw. usw. Und wenn Sie eine Konfiguration hinzufügen müssen, kann es erforderlich sein, das Gerät offline zu nehmen, die Konfiguration hinzuzufügen und es dann online zu nehmen. Solche Dinge.

Sie wollen KEINE Tests auf niedrigem Niveau durchführen. Ihre Unit-Tests würden dann nur widerspiegeln, wie Sie annehmen dass das Gerät funktioniert, ohne etwas zu bestätigen.

Der Schlüssel dazu ist, drei Elemente zu erstellen: einen Controller, eine Abstraktion und eine Adapterimplementierung dieser Abstraktion. In Cpp, Java oder C# würden Sie entweder eine Basisklasse oder eine Schnittstelle erstellen, um diese Abstraktion darzustellen. Ich gehe davon aus, dass Sie eine Schnittstelle erstellt haben. Sie zerlegen die Schnipsel in atomare Operationen. Zum Beispiel erstellen Sie eine Methode namens "start" und "add(parameter)" in der Schnittstelle. Sie fügen Ihre Snippets in den Geräteadapter ein. Der Controller wirkt über die Schnittstelle auf den Adapter ein.

Identifizieren Sie Teile der Logik innerhalb der Snippets, die Sie im Adapter platziert haben. Dann müssen Sie entscheiden, ob diese Logik auf niedriger Ebene angesiedelt ist (Details zur Protokollbehandlung usw.) oder ob es sich um Logik handelt, die in den Controller gehört.

Sie können dann in zwei Stufen testen: * Eine einfache Test-Panel-Anwendung, die auf den konkreten Adapter wirkt. Dies wird verwendet, um zu bestätigen, dass der Adapter tatsächlich funktioniert. Dass er startet, wenn Sie "Start" drücken. Dass das Gerät wie erwartet reagiert, wenn Sie z.B. nacheinander "offline gehen", "transmit(192)" und "online gehen" drücken. Dies ist Ihr Integrationstest.

Sie führen keinen Unit-Test der Details im Adapter durch. Sie testen ihn manuell, weil das einzige Erfolgskriterium die Reaktion des Geräts ist.

Allerdings ist die Controller ist vollständig einheitlich getestet. Es besteht nur eine Abhängigkeit von der Abstraktion, die in Ihrem Testcode gespottet wird. Ihr Code ist also nicht von Ihrem Gerätetreiber abhängig, da der konkrete Adapter nicht beteiligt ist.

Dann schreiben Sie Unit-Tests, um zu bestätigen, dass z. B. die Methode "Add(1)" tatsächlich "Go offline", dann "Transmit(1)" und dann "Go online" auf der Mocked-Out-Abstraktion aufruft.

Die Herausforderung besteht hier darin, zwischen dem Adapter und dem Controller zu unterscheiden. Was gehört wohin? Für mich hat es sich bewährt, zuerst das oben erwähnte Testpanel zu erstellen und dann das Gerät damit zu manipulieren.

Der Adapater sollte die Details ausblenden, die Sie nur ändern müssen, wenn sich das Gerät ändert.

  1. Wenn das Bedienfeld umständlich zu bedienen ist und viele Sequenzen immer wieder wiederholt werden müssen oder wenn sehr gerätespezifische Kenntnisse für die Bedienung des Bedienfelds erforderlich sind, dann haben Sie eine zu hohe Granularität und sollten einige von ihnen zusammenfassen. Das Testfeld sollte sinnvoll sein.

  2. Wenn sich die Anforderungen der Endbenutzer ändern, hat dies Auswirkungen auf den Adaptercode, dann haben Sie wahrscheinlich eine zu geringe Granularität und sollte die Operationen aufteilen, so dass die Änderung der Anforderungen durch testgetriebene Entwicklung in der Controller-Klasse berücksichtigt werden kann.

2voto

philant Punkte 32877

Ich würde sie für anwendungsbezogene Tests empfehlen. Auch wenn der Aufbau des Gerüsts schwierig und kostspielig sein kann, gibt es hier eine Menge zu gewinnen:

  • Absturz nur eines Prozesses im Gegensatz zu einem System
  • Fähigkeit zur Verwendung von Standardwerkzeugen (Debugger, Speicherüberprüfung ...)
  • die Beschränkung der Hardware-Verfügbarkeit zu überwinden
  • schnelleres Feedback: keine Installation im Gerät, nur kompilieren und testen
  • ...

Was die Namensgebung betrifft, so kann dies als Komponententest bezeichnet werden.

Die Anwendung kann den Gerätetreiber entweder auf die gleiche Weise initialisieren wie das Zielbetriebssystem oder direkt die Interna des Treibers verwenden. Ersteres ist teurer, führt aber zu einer größeren Abdeckung. Dann wird der Linker feststellen, welche Funktionen fehlen, und sie ersetzen, möglicherweise mit Explosionsstummel .

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X