Empfohlenes Webinar: MISRA C++ 2023: Alles, was Sie wissen müssen | Zum Video

Wann sollte man C/C++-Code für Unit-Tests nachahmen?

Headshot von Miroslaw Zielinski, Direktor Produktmanagement bei Parasoft
2. Juni 2023
7 min lesen

Beim Unit-Testen handelt es sich um den Prozess der Trennung von Einheiten und der Durchführung unabhängiger Tests für jede einzelne. Dieser Artikel enthält umfassende Anleitungen zum Mock-Unit-Test und einige hilfreiche Hinweise für C- und C++-Unit-Tests.

Bei Unit-Tests geht es darum, isolierte Einheiten oder Funktionen und Abläufe zu testen. In diesem Beitrag befassen wir uns mit verschiedenen Gelegenheiten zum Spotten und gehen dabei auch auf einige häufig gestellte Fragen ein, um Ihnen eine effektive Anleitung zu geben C- und C++-Unit-Tests.

Wenn ich mit einem Beispiel beginne, wird mir oft die Frage gestellt: „Wie viel Unit-Test-Isolation brauchen wir?“ Dies ist eine wiederkehrende und wichtige Frage, die häufig bei der Entwicklung von Komponententests für C und C++ diskutiert wird.

Ich spreche hier nicht von der Isolation durch den Kollegen, der neben uns im offenen Raum sitzt und den Rhythmus der Musik über seine Kopfhörer trommelt, was übrigens auch sehr wichtig ist, wenn wir gute Qualität schaffen wollen Code. Ich spreche von der Isolierung des getesteten Codes von seiner Umgebung – seinen sogenannten Kollaborateuren.

Bevor ich fortfahre, möchte ich noch eines klarstellen. Bei der Erörterung von Stubbing und Mocking für C- und C++-Sprachen wird normalerweise eine Grenze zwischen C und C++ gezogen, da sich die Unterschiede in der Sprachebene in der Komplexität, den Fähigkeiten und den Erwartungen typischer Mocking-Frameworks widerspiegeln.

Bei Parasoft C/C++test ist die Situation etwas anders, da die meisten Framework-Funktionen für beide Sprachen verfügbar sind. Wenn ich dieses Thema erörtere, werde ich also entweder ein C- oder ein C++-Unit-Test-Beispiel geben. Sofern ich nicht ausdrücklich etwas als nur für C oder C++ unterstützt markiert habe, sollten Sie immer davon ausgehen, dass bestimmte Funktionen für beide Sprachen bereitgestellt werden.

Was ist Mocking und wie funktioniert es beim Testen?

Die Definitionen sind bekanntermaßen inkonsistent und haben für einige Verwirrung gesorgt. Daher ist hier eine kurze Erklärung dessen, was ein Stub ist, angebracht. Dann ist es einfacher zu erklären, was ein Mock ist.

Der Zweck beider besteht darin, einen Codeabschnitt oder eine Abhängigkeit außerhalb der Einheit zu ersetzen. Durch die Eliminierung aller Abhängigkeiten einer Einheit oder Funktion können Sie sich bei Ihren Tests auf die Prüfung der Qualität (Sicherheit und Zuverlässigkeit) der Einheit konzentrieren.

Stubbing ersetzt Abhängigkeiten, ist aber eine einfache Implementierung, die vorgefertigte Werte zurückgibt. Mocks konzentrieren sich auf das Verhalten und sind daher eine Implementierung, die durch den Unit-Test gesteuert wird. Sie können mit Rückgabewerten implementiert werden, Werte von Argumenten überprüfen und dabei helfen, die Funktionalität allgemeiner Sicherheitsanforderungen zu überprüfen. Aber ganz ehrlich: Wenn ich meine Unit-Testfälle erstelle, denke ich nicht darüber nach, ob ich stumpfe oder mich lustig mache. Ich konzentriere mich lediglich darauf, die Funktionalität zu testen, um festzustellen, ob sie meinen Anforderungen entspricht und ob die Implementierung robust ist.

Isolieren oder nicht isolieren?

Manchen Menschen gebietet der gesunde Menschenverstand, dass wir uns nicht isolieren sollten, es sei denn, wir haben einen guten Grund dafür. Das Testen durch Einbeziehung anderer Mitarbeiter oder Funktionen erhöht nur unsere Durchdringung der Codebasis. Und warum sollten wir uns die Gelegenheit entgehen lassen, zusätzliche Codeabdeckung zu erhalten und Fehler außerhalb der Einheit zu finden? Nun ja, dafür gibt es gute Gründe.

Ein orthodoxer Unit-Tester wird das argumentieren Beim Unit-Test geht es darum, die isolierten Units zu testen und das soll auch so bleiben. Wenn jede einzelne Einheit gesund ist, stärkt das nur das Ganze. Darüber hinaus wird das Testen mit echten Mitarbeitern oder das Einbeziehen anderer Funktionen in den Test als Integrationstest bezeichnet. Wenn wir den gesamten Code einbeziehen, werden Testfälle als Systemtests bezeichnet.

Es gibt verschiedene Abstraktionsebenen. Entwickler und Tester sollten Tests auf diesen verschiedenen Ebenen durchführen. Die unterste Ebene wird als Unit-Test bezeichnet und Sie müssen eine Isolierung der Einheit durchführen. Lassen Sie uns einige Gründe für Spott und Best Practices bei der Isolierung von Einheiten durchgehen.

Gründe, in Ihrem Testprozess Scheineinheiten zu verwenden

1. Mitarbeiter noch nicht implementiert oder noch in der Entwicklung

Dies ist eine einfache. Wir haben keine Wahl und brauchen eine Scheinimplementierung. Das folgende Diagramm zeigt diese typische Unit-Test-Umgebung (SUT - Testsystem, DOC - abhängige Komponente / Mitarbeiter):

2. Hardware-Unabhängigkeit

Für Entwickler, die Desktop-Anwendungen schreiben, mag diese Art von Problemen weit entfernt erscheinen. Aber für Embedded-Entwickler ist die Hardware-Unabhängigkeit von Unit-Tests ein wichtiger Aspekt, der eine Testautomatisierung und -ausführung auf hohem Niveau ermöglicht, ohne dass Hardware erforderlich ist.

Ein gutes Beispiel hierfür wäre ein Testgerät, das mit GPS-Hardware interagiert und die Bereitstellung einer bestimmten Folge von Lokalisierungskoordinaten zur Berechnung der Geschwindigkeit erwartet. Obwohl es eine gute Idee ist, dass wir mehr Sport treiben, kann ich mir nicht vorstellen, dass Tester jedes Mal, wenn eine Unit-Test-Sitzung erforderlich ist, mit einem Gerät herumlaufen, um Bewegungen zu simulieren, nur um die erforderlichen Testeingaben zu generieren. Zu diesem Zweck veranschaulicht dieses Beispiel, wie spät im Entwicklungslebenszyklus GPS-Tests eines Geräts erfolgen würden, wenn während der Entwicklung keine Hardwareunabhängigkeit möglich wäre.

3. Fehlerinjektion

Das absichtliche Einschleusen von Fehlern ist ein häufiges Szenario beim Testen. Dies kann beispielsweise verwendet werden, um zu testen, ob die Speicherzuweisung fehlgeschlagen ist, oder um festzustellen, ob eine Hardwarekomponente ausgefallen ist. Einige Entwickler versuchen, den echten Mitarbeiter in der Testinitialisierungsphase so zu stimulieren, dass er mit einem Fehler reagiert, wenn er vom getesteten Code aufgerufen wird. Für mich ist das nicht praktikabel und meist zu aufwändig. Eine testspezifische, gefälschte Implementierung, die einen Fehler simuliert, ist normalerweise die viel bessere Wahl.

Best Practices für die Implementierung von Mocking in Ihrem Testprozess

Neben diesen offensichtlichen Fällen, in denen ein verspotteter Mitarbeiter immer erwünscht ist, gibt es einige andere, subtilere Situationen oder Best Practices, in denen es eine gute Wahl ist, gefälschte Mitarbeiter zu verspotten oder hinzuzufügen. Wenn bei Ihrem Testprozess eines der unten aufgeführten Probleme auftritt, ist dies ein Hinweis darauf, dass eine bessere Isolierung des getesteten Codes erforderlich ist.

1. Wenn Unit-Tests nicht wiederholbar sind

Vergänglichkeit kann ein Problem sein, das die Implementierung stabiler Tests erschwert. Es gibt Fälle, in denen Einheiten auf ein externes Signal angewiesen sind, um ihr Verhalten anzuzeigen. Ein klassisches Beispiel ist eine Einheit, deren Verhalten sich auf die Systemuhr verlässt. Reagiert eine Einheit beispielsweise zu bestimmten Zeitpunkten unterschiedlich, ist eine Automatisierung nur schwer zu erreichen. Eine bewährte Methode besteht darin, den Aufruf der Systemuhr zu simulieren und die volle Kontrolle über die Zeiteingabewerte zu erhalten.

2. Wenn Testumgebungen schwer zu initialisieren sind

Das Initialisieren der Testumgebung kann sehr komplex sein. Es kann eine entmutigende, wenn nicht sogar unmögliche Aufgabe sein, die tatsächlichen Mitarbeiter so zu simulieren, dass sie zuverlässige Eingaben in den getesteten Code liefern.

Komponenten hängen oft miteinander zusammen. Wenn wir versuchen, ein bestimmtes Modul zu initialisieren, initialisieren wir möglicherweise die Hälfte des Systems. Das Ersetzen der echten Mitarbeiter durch eine Schein- oder Fake-Implementierung verringert die Komplexität der Initialisierung der Testumgebung.

3. Wenn der Teststatus schwer zu bestimmen ist

In vielen Fällen muss zur Bestimmung des Testurteils der Status des Mitarbeiters überprüft werden, nachdem der Test ausgeführt wurde. Bei echten Kollaborateuren ist es oft unmöglich, da es in der Schnittstelle für echte Kollaborateure keine geeignete Zugriffsmethode gibt, um den Status nach dem Test abzufragen.

Das Problem wird normalerweise behoben, indem ein echter Mitarbeiter durch einen Schein ersetzt wird. Wir können die Fake-Implementierung mit allen möglichen Zugriffsmethoden erweitern, um das Testergebnis zu ermitteln.

4. Wenn Tests langsam sind

Es gibt Fälle, in denen eine Antwort des echten Mitarbeiters eine beträchtliche Zeit in Anspruch nehmen kann. Es ist nicht immer klar, wann die Verzögerung inakzeptabel wird und wann eine Isolierung erforderlich ist. Ist eine Verzögerung von zwei Minuten bei einem Testlauf akzeptabel oder nicht?

Es ist oft wünschenswert, Testsuiten so schnell wie möglich ausführen zu können, möglicherweise nach jeder Codeänderung. Große Verzögerungen aufgrund der Interaktion mit echten Mitarbeitern können dazu führen, dass Testsuiten zu langsam sind, um praktisch zu sein. Nachbildungen dieser echten Mitarbeiter können um mehrere Größenordnungen schneller sein und die Testausführungszeit auf ein akzeptables Niveau bringen.

Praktisches Beispiel dafür, wann Unit-Tests von C/C++-Code simuliert werden sollten

Das Verspotten von Schnittstellen kann die Testarbeit erheblich erleichtern. Anstatt dass Ihre Einheit andere aufruft, kann sie eine Scheinschnittstelle aufrufen. Ihr Testcode kann sich auf allen Seiten der Einheit, die Sie testen möchten, einfügen und dann alle Ausgaben untersuchen und alle Eingaben verarbeiten.

Nehmen wir an, dass wir im folgenden Codebeispiel die Bar-Einheit/Funktion testen und sie eine Fake/Mock-Foo-Funktion aufrufen lassen möchten. Dazu benötigen wir eine Möglichkeit, den Aufruf von foo() zu ersetzen und zu steuern. Dafür gibt es mehrere Spott-Ansätze und Parasoft kann einen Großteil davon für Sie automatisieren. In diesem Beispiel wird der Einfachheit halber ein Makro (#define FOO) verwendet, um den Collaborator foo() zu steuern.

#ifdef TEST
#define FOO mock_foo
#else
#define FOO foo
#endif
 
int mock_foo(int x)
{
	return x;
}
 
int bar(int x)
{
	int result = 0;
	for (int i = 0; i < 10; i++)
	{
		result += FOO(i + x);
	}
	return result;
}

Hilfreiche Fragen, um zu entscheiden, ob man sich lustig machen soll oder nicht

Berücksichtigen Sie die folgenden vier Fragen, wenn Sie einen neuen C- oder C++-Komponententest schreiben und sich für die Verwendung von Originalmitarbeitern oder simulierten Implementierungen entscheiden.

  1. Ist der echte Mitarbeiter eine Risikoquelle für die Stabilität meiner Tests?
  2. Ist es schwierig, den echten Mitarbeiter zu initialisieren?
  3. Ist es möglich, den Status des Mitarbeiters nach dem Test zu überprüfen, um den Teststatus zu bestimmen?
  4. Wie lange dauert es, bis der Mitarbeiter antwortet?

Wenn wir den Mitarbeiter gut genug kennen, um alle diese Fragen beantworten zu können, fällt uns die Entscheidung in die eine oder andere Richtung leicht. Wenn nicht, würde ich vorschlagen, mit dem echten Mitarbeiter zu beginnen und nach und nach zu versuchen, die vier Fragen zu beantworten. In der Praxis ist dies der Ansatz, den die meisten Praktiker der testgetriebenen Entwicklung (TDD) in ihrer täglichen Arbeit anwenden. Das bedeutet, dass Sie Ihre Testfälle sorgfältig behandeln und sorgfältig überprüfen müssen, bis sie stabil sind.

In den meisten Fällen wird die Isolierung von Unit-Tests durch die Abhängigkeiten der zu testenden Unit erschwert. Es gibt eindeutig wünschenswerte Fälle, in denen das Verspotten einer abhängigen Komponente erforderlich ist, aber auch subtilere Situationen. In einigen Fällen ist dies nicht eindeutig und hängt vom Risiko und der Unsicherheit ab, die eine Abhängigkeit in der Testumgebung birgt.

So optimieren Sie Unit-Tests für eingebettete und sicherheitskritische Systeme