Forum: Compiler & IDEs Wie testet ihr eure Software ?


von Thomas W. (wagneth)


Lesenswert?

Hallo !

Die eigentliche Frage steht ja schon im Betreff.

Mir geht es darum ein paar Regeln zu sammeln um Fehler festzustellen 
bzw. im Ansatz schon zu erkennen.

Sowas wie keine Zuweisungen in Ifs oder den Wertebereich prüfen.

Was für "Tricks" habt ihr euch so angewöhnt um Fallstricke zu umschiffen 
?

Was macht Ihr gegen Laufzeitfehler ?
Schreibt ihr sowas wie Testbedingungen für eure Funktionen ?

von Mehmet K. (mkmk)


Lesenswert?


von Klaus W. (mfgkw)


Lesenswert?

Sehr hilfreich ist die Angewohnheit, sich schon im Vorfeld
klar zu werden, was eine Funktion können soll.
Damit drängt sich direkt auf, wie man sie testen kann - nämlich
genau gegen diese Soll-Vorgaben.
Bei Auftragsarbeiten sollte es hier eine gewisse Verbindung
zum Pflichtenheft geben...

Besonders bei kleineren Sache schreibe ich dann zu den Funktionen
auch gleich in die selbe Datei den Testcode rein, z.B.
1
// dieses ... macht folgendes: ...
2
3
#include "meine_funktionen.h"
4
5
void meinefunkltion1(){...}
6
void meinefunkltion2(){...}
7
void meinefunkltion3(){...}
8
9
#ifdef TESTMEINEFUNKTIONEN
10
11
#include <iostream>
12
13
int main( int nargs, char**args )
14
{
15
   // Funktionen aufrufen, Ergebnisse prüfen...
16
   // das ergibt sich 1:1 aus den Anforderungen
17
   // ...
18
}
19
20
#endif TESTMEINEFUNKTIONEN

Zumindest bis zu einer gewissen Größe ist das praktisch, weil man
dann den einen Quelltext mit -DTESTMEINEFUNKTIONEN direkt zu
einem Testprogramm kompilieren kann, ohne -DTESTMEINEFUNKTIONEN
wird es in einem größeren Programm als Objekt verwendet ohne die
Testaufrufe.

Soweit sich Tests automatisieren lassen, habe ich dafür
gelegentlich ein eigenes Target im Makefile.

Zudem verwende ich viel assert() (nicht auf MC) und nutze
reichlich Ausnahmen, die mir auch Datei und Ort des Entstehens
und der beim Werfen durchlaufenen Ebenen sammeln.

Über den Präprozessor habe ich weiterhin meist eine Testversion,
die mehr testet und mehr ausgibt, als die endgültige Version.

Sehr sinnvoll ist es auch, nach Möglichkeit ein und denselben
Quelltext mit möglichst verschiedenen Compilern zu testen.
Das offenbart etliche kritische Stellen.
Hilft jetzt natürlich nicht bei reinen MC-Geschichten.
Dort kann und sollte man aber auch oft die systemspezifischen
Dinge (PORT...) von den allgemeineren Funktionen (PID-Regler,
Numerik, ...) trennen und letztere einzeln auf anderen
Systemen testen.
Z.B. kann man für etwas wie einen PID-Regler, der zuletzt auf
einem MC laufen soll, prima ein Testprogramm als Simulation
auf einem PC bauen; das eröffnet viele Erkenntnisse.

Beim Stil des Quelltextes ("niemals ..., weil das böse ist")
bin ich etwas reserviert, weil es mir oft zu dogmatisch ist.
Aber auf jeden Fall soll der Quelltext so sauber und aufgeräumt
wie nur irgend möglich sein, und auf den ersten Blick
verständlich.
Nur so sieht man auch die Fehler mit vertretbarem Zeitaufwand.

Dazu gehören Einrückungen, Klammern etc.; vor allem sprechende
Namen.
Auch bei der Struktur eines Programms (Threads, wer plaudert
wie mit wem etc.) muß man sich im Klaren sein, was man will und
das auch im Stil und der Dokumentation deutlich ausdrücken.

Alleine der Übersichtlichkeit wegen würde ich Zuweisungen
deshalb eher weniger direkt in if() schreiben, pauschal
ausschließen möchte ich es aber nicht.
Zumindest in while-Bedingungen mache ich schon gelegentlich
auch gleich eine Zuweisung, weil es den Quelltext manchmal
deutlich vereinfacht (while( (c=fgetc(f)) ){...}).

In dem Stil kann man jetzt natürlich vom Hundertsten ins
Tausendste kommen...

Es gibt dazu auch durchaus interessante Bücher, die meist
nicht zu 100% auf den eigenen Fall übertragbar sind, aber
manchmal doch lesenswert.
Konkret für C fällt mir da ein "The Practice of Programming"
von Kernighan und Pike.

von Volker Z. (vza)


Lesenswert?

1) ich spicke meinen Queltext mit einem selbst gestrickten 
ASSERT-macros,
das nur in der Debugversion aktiv ist.
1
#ifdef DEBUG
2
# define ASSERT(test) { \
3
   if (!(test)) { \
4
      fprintf(stderr, "Assertion failed: '%s' ( %s line %d)\n\r", \
5
         #test, __FILE__, __LINE__); \
6
      asm("brk"); \
7
      } \
8
   }
9
#else
10
# define ASSERT(test) 
11
#endif
12
13
...
14
15
void foo(int *ptr)
16
{
17
   ASSERT(ptr!=NULL);
18
  ...
19
}

2)  teste ich modular, so das ich mich 99% sicher bin das die 
verwendeten Unterfunktionen funktionieren. Eigentlich schreibe ich fast 
immer erst den Testcode und dan die Unterfunktionen. zB.

foo.c
1
int foo(int x)
2
{
3
...
4
}
5
6
#ifdef MODULTEST
7
void main(void)
8
{
9
   ASSERT(foo(3));
10
}
11
#endif

3) simuliere ich viel auf dem PC. Wegen der bessere Debugmöglichkeiten. 
Ja auch LED-Ausgaben, eeprom-Routinen und Timmer-Interrupts.
Wie ?
 - Port-ausgaben landen in einer normalen Variablen.
 - eeprom zugriffe lenke ich auf die Festplatte um.
 - Für periodische Timmer nehme ich einen eigenen Thread. (nicht so 
genau aber zum Testen reicht es meist)

So kann ich 60-90% der Code auf einem PC simulieren und Testen.


ciao Volker

von Gast (Gast)


Lesenswert?

Hi,

um "Fehler im Ansatz zu erkennen", helfen Dir Tests bzw. Testmethoden 
nicht, da diese ja erst nach der Implementierung gemacht werden können. 
Hier helfen ein vernünftiges Anforderungsmanagement ("Wissen, was wie zu 
tun ist") und ein Design (z.B. Flowchart, Architektur).

Um das Verhalten einzelner Funktionen zu testen, gibt es sog. 
Unit-Testtools (z.B. CUNIT); diesen gibtst Du Deine Funktion plus einer 
Liste von konkreten Eingangsparametern und den erwarteten Ausgaben. 
CUNIT lässt die Funktion dann laufen und vergleicht die tatsächliche mit 
der erwarteten.

von Gast (Gast)


Lesenswert?

Wollte schreiben:

"CUNIT lässt die Funktion dann laufen und vergleicht die tatsächliche 
Ausgabe mit der erwarteten."

von Thomas W. (wagneth)


Angehängte Dateien:

Lesenswert?

Hmmm,
also ich habe mir mehr intuitiv angewöhnt die Aufgabenstellung als 
Problem anzusehen das ich mit Teilmengen von Lösungen angehe.

Dieses Problem teile ich in immer kleiner Teilprobleme bzw Teillösungen,
die für mich noch überschaubar sind auf.

Ich denke das man so eine gute Abstraktion hinbekommt.
Allerdings lerne ich beim Programmieren (für mich reinhacken des 
Source),
immer wieder neues über "meine" Probleme dazu.

Man könnte sagen mein Wissen um die Lösungen steigt ?!? :)

Glaube auch das die Teilmengen einfacher zu testen sind...

----
@Mehmet Kendi

Ich habe mir mal dieses Splint angesehen...
Das Produziert schon einen gawaltigen Output ;) :0

Alleine bei obigem Source.

Aber das meckert ja schon wenn man anstelle eines >int< einen >uint8_t< 
für einen Boolean "missbraucht".

Bzw. mit sowas kann ich gerade im moment noch nichts anfagen :

>Function exported but not used outside main: adc_init
>   main.c:107:1: Definition of adc_init


---

@Klaus Wachtler

Dieses
1
assert();
 prüft als nur den Rückgabewert einer Funktion im Laufenden Betrieb.

Wenn ich eine Funktion auf Herz und Nieren Testen will,
habe ich in C keine andere Möglichkeit als aus einem (ev. bedingt 
kompilierten) Programmteil meine Funktion mit verschiedenen 
Werten/Zuständen aufzurufen und die erwarteten Ergebnisse zu prüfen.

Das blöde ist ja, ich muss sehr viele Bedingungen durchspielen.
Allerdings müssen die Testfälle dann auch ohne Bezug auf die zu Prüfende 
Funktion sein.

(Das kann schon Spaghetti im Kopf machen, oder ?)

Selbst bei einem einfachen (a > b) - Vergleich können meine Testfälle 
einfach ausfallen :

10 > 5     -> wahr
10 > 10    -> falsch
5 > 10     -> falsch

Da mein {a,b} vielleicht vom Typ int16_t ist,
müsste ich für meinen Test (heisst das Validierung?) doch den gesamten 
Wertebereich und alle Kombinationen austesten ?!??

---
@Volker Zabe

Wow !

Das bläht den Umfang doch bestimmt auf das 1,5fache auf, oder ?
OK, und es reduziert die Fehleranfälligkeit enorm.

Wobei ein Testfall auch nur so gut wie der Programmierer ist...


>2)  teste ich modular, so das ich mich 99% sicher bin das die
>verwendeten Unterfunktionen funktionieren. Eigentlich schreibe ich fast
>immer erst den Testcode und dan die Unterfunktionen. zB.

Es scheint also auch bei den Informatikern beliebt zu sein die Teilmenge 
der Lösung auf die kleinste Abstraktionsebene zu bringen.

---
@ Gast

Dieses CUnit werde ich mir noch ansehen,
dafür hat es bis jetzt noch nicht gereicht.

----


Hat denn vielleicht jemand ein kleines Progrämmelchen um mir ein 
Praktisches Beispiel zu geben ?

Das scheint ja ein weites feld zu sein...

Vielen Dank schonmal für die guten Antworten !!!

Thomas


PS: Ich schreibe halt nur in C auf dem µC. Wobei ich merke das ich mich 
damit wieder befassen müsste !

von Klaus W. (mfgkw)


Lesenswert?

Thomas W. schrieb:
> ...
> @Klaus Wachtler
>
> Dieses
1
assert();
 prüft als nur den Rückgabewert einer Funktion im
> Laufenden Betrieb.

Es prüft einen beliebigen Ausdruck, je nachdem, was man reinschreibt.
Dadurch hat man natürlich keine umfassende Kontrolle, ob ein Programm
alles richtig macht, sondern viele sinnvoll gesetzte assert() sind
nur ein Mosaiksteinchen, um Fehler zu vermeiden.

Gedacht ist es für Fälle, in denen man an irgendeiner Stelle
davon ausgeht, daß bestimmte Bedingungen doch hoffentlich auch erfüllt
sind, ohne sie immer tatsächlich zur Laufzeit testen zu wollen.
Beispiel: in einer Funktion, der ein Zeiger übergeben wird, will
man (aus Effizienzgründen) nicht immer testen, ob ein übergebener
Zeiger !=NULL ist, sondern will das beim Aufrufer einfach voraussetzen.
In der Debugphase kann man das in der Funktion dennoch mit assert()
testen (assert(p_irgendwas!=NULL)), ohne daß es in der ausgelieferten
Version Rechenzeit kostet.

assert() ist sicher kein Ersatz, um Benutzereingaben zu testen
oder ähnlich unkontrollierbares Verhalten.


>
> Wenn ich eine Funktion auf Herz und Nieren Testen will,
> habe ich in C keine andere Möglichkeit als aus einem (ev. bedingt
> kompilierten) Programmteil meine Funktion mit verschiedenen
> Werten/Zuständen aufzurufen und die erwarteten Ergebnisse zu prüfen.
>
> Das blöde ist ja, ich muss sehr viele Bedingungen durchspielen.
> Allerdings müssen die Testfälle dann auch ohne Bezug auf die zu Prüfende
> Funktion sein.

Ein voller Test alle Eingabemöglichkeiten scheitert in aller Regel
an der Menge der Möglichkeiten.

> ...
> Das scheint ja ein weites feld zu sein...

Ja.

> ...

von Karl H. (kbuchegg)


Lesenswert?

Klaus Wachtler schrieb:

> Gedacht ist es für Fälle, in denen man an irgendeiner Stelle
> davon ausgeht, daß bestimmte Bedingungen doch hoffentlich auch erfüllt
> sind, ohne sie immer tatsächlich zur Laufzeit testen zu wollen.

assertions können auch dann sinnvoll sein, wenn man den bewussten 
Laufzeittest auch tatsächlich macht um Abstürze bzw. Fehlverhalten zu 
vermeiden. Die Assertion schlägt dann an und teilt mir frühzeitig mit, 
dass die Funktion fehlerhaft aufgerufen wurde. Die Alternative wäre 
langwieriges Suchen, wo ein Sicherheitsmechanismus zugeschlagen hat und 
Schlimmerers verhindert hat, die globale Funktion jedoch trotzdem nicht 
ausgeführt wurde.

> assert() ist sicher kein Ersatz, um Benutzereingaben zu testen
> oder ähnlich unkontrollierbares Verhalten.

Das sicher nicht.
Aber assertions können schon sehr hilfreich sein, weil man nicht 
zufällig über Fehler stolpert, sondern sich der Code selber meldet, dass 
etwas nicht stimmt.

von Thomas W. (wagneth)


Lesenswert?

OK, diese Asserts benachrichtigen mich bei einer nicht erfüllten 
Erwartungshaltung.

Das hat ein wenig von LED-Debugging...

---

Kann man sagen, selbst wenn man alle Teilmengen (Funktionen) eines 
C-Programms geprüft hätte (was anscheinend nur begrenzt möglich ist, 
obiges bsp. mit dem einfachen Vergleich würde ja bedeuten 2^32 Zustände 
zu prüfen),
kann man immer noch nicht sicher sein das dass ganze Programm Fehlerfrei 
ist ?

Ich kann mir ja sogar die dollsten Fehler reinzaubern,
wenn ich in meinen defines einfach mal einen Wertebereich 
überschreite...
(z.B. Bedingung wird nie mehr erfüllt weil Variable nicht weit genug 
Zählt)

Theoretisch müsste man bei allem unbekannten den Wertebereich abfragen.


...scheint wohl einer der Gründe zu sein, warum man Informatik studiert 
und nicht als Ausbildung erlernt.

von Thomas W. (wagneth)


Lesenswert?

Das erinnert mich stark an : http://de.wikipedia.org/wiki/Falsifizierung

Wir erarbeiten etwas im guten,
um es danach kaputt zu prüfen...

von Karl H. (kbuchegg)


Lesenswert?

Thomas W. schrieb:

> Kann man sagen, selbst wenn man alle Teilmengen (Funktionen) eines
> C-Programms geprüft hätte (was anscheinend nur begrenzt möglich ist,
> obiges bsp. mit dem einfachen Vergleich würde ja bedeuten 2^32 Zustände
> zu prüfen),
> kann man immer noch nicht sicher sein das dass ganze Programm Fehlerfrei
> ist ?

Das ist richtig.

Denn es gilt:
Durch testen kann man immer nur das Vorhandensein von Fehlern 
nachweisen, nie das Nichtvorhandensein.

Theoretisch ginge auch die Umkehrung (Nachweis der Fehlerfreiheit) 
allerdings ist es normalerweise nicht praktikabel, weil du in einem 
realen Programm nicht alle überhaupt möglichen Inputs durchtesten 
kannst.

von Thomas W. (wagneth)


Lesenswert?

... zumal die Blackbox (unser Programm) nicht nur ein Verhalten abhängig 
zu den Eingangszuständen und deren Feedback besitzt,
sondern auch noch abhängig von der Zeit ist.

Somit ist doch prinzipiell schon bewiesen das nach dem jetzigen Stand es 
prinzipiell  immer zu fehlerhafter Programmausführung kommen kann -- 
auch wenn der Algorithmus noch so gut ist.

Ich als Programmierer, bei meinem Wissen kann ich nur ein schlechter 
sein, muss also dafür sorgen das Fehler die zu unbekannten Zuständen 
(???) führen können irgendwie abgefangen werden und in einem 
kontrollierten enden.

z.B. Neustart ausführen, unkritische Defaultwerte nehmen (Notlauf), ...

(der Freibrief für den MS-BSOD)

Bei obiger Alarmanlage sieht das alles noch recht überschaubar aus,
aber immer wenn ich nach kurzer Zeit wieder mal drüberschaue fallen mir 
irgendwelche Unzulänglichkeiten auf.

z.B. wäre die Sensor.Codeschloss - abfrage als Funktion die automatisch 
die Variable zurücksetzt mit Sicherheit schöner...

von Karl H. (kbuchegg)


Lesenswert?

Thomas W. schrieb:

> Somit ist doch prinzipiell schon bewiesen das nach dem jetzigen Stand es
> prinzipiell  immer zu fehlerhafter Programmausführung kommen kann --
> auch wenn der Algorithmus noch so gut ist.

Na ja, ganz so ist es nicht.
Man kann schon mathematisch beweisen, ob ein Algorithmus korrekt ist. 
Das Problem ist die Definition der Korrektheit. Sie wird dadurch 
bestimmt, dass man einen Satz von Ausgangseigenschaften (mathematisch) 
festlegt, sowie einen Satz von Endeigenschaften. Man kann dann 
mathematisch zeigen, ob die Endeigenschaften durch anwenden des 
Algorithmuses entstehen. Das schützt natürlich nicht vor 
Implementierungsblunder, aber wenn der Algorithmus korrekt in eine 
Programmiersprache übertragen wurde, dann hat man somit den Nachweis, 
dass diese Codestelle in dem Sinne korrekt ist, dass sie die 
Anforderungen der Transformation von Ausgangseigenschaften zu 
Endeigenschaften liefert.

Solche Beweisführungen sind aber extrem aufwändig (zumindest waren sie 
das, als ich sowas zuletzt im Studium machen musste).

Was hier natürlich auch völlig aussen vor bleibt, sind die Einflüsse der 
Umgebung, wie 'End of Memory', um nur die Prominenteste zu nennen.

von Thomas W. (wagneth)


Lesenswert?

>Was hier natürlich auch völlig aussen vor bleibt, sind die Einflüsse der
>Umgebung, wie 'End of Memory', um nur die Prominenteste zu ne

^--- Umgebung in welchem Sinn ? End of Memory ? Meinst Du z.B. die 
Hardware auf der das Prg. läuft ?

Also kann man zwischen folgenden Fehlerquellen unterscheiden :

1 Ungenaue/falsche Aufgabenstellung.
2 Aufgabe nicht verstanden.
3 Algorithums -- d.h. Aufgabe/Problem falsch gelöst.
4 Fehlerhafte Verarbeitung/Implementierung : if (x=1) {..}, Overflows
5 Die Hardware auf der Ausgeführt wird. (Wobei der Programmierer dabei 
keine "schuld" hat)

Wobei 1 und 2 wohl eher Kommunikationsprobleme sind...

Kann mir jemand ein paar Bücher empfehlen ?
(wollte mir eigentlich die Übersetzung Programmieren in C kaufen...)

von Thomas W. (wagneth)


Lesenswert?

Also könnte man behaupten das jede Programmiersprache sich auf die 
Axiome der Mathematik und deren Ableitungen (wohl angefangen bei der 
Aussagenlogik, Grundrechenarten, ...) beruft.

Eine Programmiersprache ist also eine Art formal mathematisch 
beschreibende Sprache ?

Die beiden sind also verwandt ! lach

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.