Entwicklung & Code
Won’t fix! – Teil 5: Tests schaffen Vertrauen, beweisen aber nie Korrektheit
Geht es um Codequalität, ist die Code Coverage eine besonders verführerische Metrik. Sie ist leicht zu erheben, in Pipelines durchzusetzen und scheint auf den ersten Blick eine objektive Aussage darüber zu treffen, wie gründlich ein System getestet ist. Bei näherer Betrachtung zeigt sich aber, dass das Testen selbst an fundamentale Grenzen stößt, an denen auch die ausgefeilteste Coverage-Messung nichts ändern kann. Das Halteproblem aus Teil 2 spielt dabei eine neue Rolle und wie wir in Teil 3 gesehen haben, lässt sich Codequalität ohnehin nicht in eine einzige Zahl pressen.
Weiterlesen nach der Anzeige
Vor einigen Jahren entwickelte ein Entwicklungsteam meines Unternehmens eine Implementierung des Chord-Algorithmus, eines klassischen Verfahrens für strukturierte Peer-to-Peer-Netzwerke. Chord organisiert die teilnehmenden Knoten in einem virtuellen Ring und erlaubt es, Key-Value-Paare effizient zu speichern und wiederzufinden, auch wenn Knoten kommen und gehen. Das Team arbeitete sorgfältig: Alle Unit-Tests waren grün, die Code Coverage lag bei 100 Prozent.
Trotzdem hatte die Implementierung einen Bug. Wurde ein Knoten im laufenden Netzwerk abgeschossen, stürzten die übrigen Knoten dominoartig ab. Das Problem lag nicht im Code eines einzelnen Knotens, sondern im Zusammenspiel mehrerer Knoten. Bestimmte Konstellationen, in denen Nachrichten zwischen Knoten in einer ungünstigen Reihenfolge eintrafen, führten zu logischen Widersprüchen, die beim Betrachten eines einzelnen Knotens nicht erkennbar waren. Die Tests deckten jede einzelne Funktion vollständig ab, aber sie testeten nicht das, was am Ende den Ausfall verursachte.
Diese Erfahrung steht stellvertretend für ein viel allgemeineres Phänomen. Tests können viele Bugs finden, aber sie können nicht garantieren, dass keine mehr da sind. Und das ist nicht nur eine pragmatische Einschränkung, sondern hat tiefer liegende Ursachen.

Golo Roden ist Gründer und CTO von the native web GmbH. Er beschäftigt sich mit der Konzeption und Entwicklung von Web- und Cloud-Anwendungen sowie -APIs, mit einem Schwerpunkt auf Event-getriebenen und Service-basierten verteilten Architekturen. Sein Leitsatz lautet, dass Softwareentwicklung kein Selbstzweck ist, sondern immer einer zugrundeliegenden Fachlichkeit folgen muss.
Die Serie „Wont‘ fix“ behandelt Probleme in der Softwareentwicklung, die sich nicht wegoptimieren lassen, mit denen man aber lernen kann umzugehen:
Was Code Coverage wirklich aussagt
Code Coverage misst, wie viel Prozent des Codes durch Tests ausgeführt werden. Eine Zeile, die mindestens einmal im Rahmen eines Tests durchlaufen wird, gilt als abgedeckt. Das klingt zunächst sinnvoll, denn nicht ausgeführter Code ist mit Sicherheit nicht getestet. Die Umkehrung gilt aber nicht: Ausgeführter Code ist nicht automatisch geprüfter Code.
Ein einfaches Beispiel zeigt das Problem. Eine Funktion gibt eine Liste sortiert zurück. Ein Test ruft diese Funktion mit einer beliebigen Liste auf, prüft aber nicht das Ergebnis. Die Coverage-Messung stellt fest, dass alle Zeilen der Funktion durchlaufen wurden, und meldet 100 Prozent Abdeckung. Trotzdem hätte die Funktion auch eine zufällige Permutation zurückgeben können, ohne dass der Test es bemerkt hätte. Coverage misst Ausführung, nicht Verifikation.
Weiterlesen nach der Anzeige
Doch selbst wenn die Tests ihre Ausgaben gewissenhaft prüfen, bleibt eine zweite Lücke. Coverage misst die Strukturen des Codes, nicht die Interaktionen zwischen Komponenten. Im Chord-Beispiel waren die einzelnen Funktionen jedes Knotens vollständig abgedeckt, aber das Zusammenspiel mehrerer Knoten in spezifischen zeitlichen Konstellationen war es nicht. Was Coverage nicht erfasst, ist die kombinatorische Explosion der möglichen Reihenfolgen, in denen Nachrichten ausgetauscht werden, in denen Threads parallel laufen, in denen Knoten verbunden oder getrennt sind. Eine Coverage von 100 Prozent ist deshalb keine Aussage über die Korrektheit des Systems, sondern eine Aussage darüber, dass kein Code übersehen wurde.
Dijkstras Bonmot
Diese strukturelle Asymmetrie zwischen Tests und Korrektheit hat Edsger Dijkstra bereits 1969 auf einer NATO-Konferenz zur Softwaretechnik in einem inzwischen berühmten Satz auf den Punkt gebracht: Testing shows the presence, not the absence of bugs. Wenig später formulierte er die ausführlichere Variante in seinen „Notes on Structured Programming“: Programmtests können benutzt werden, um die Anwesenheit von Fehlern zu zeigen, niemals aber deren Abwesenheit.
Die Aussage klingt banal, hat aber weitreichende Konsequenzen. Ein Test, der einen Bug findet, beweist die Existenz dieses Bugs. Ein Test, der keinen Bug findet, beweist nichts darüber, ob weitere Bugs existieren. Die übliche Beweisstruktur in den Naturwissenschaften, dass das Ausbleiben eines Gegenbeispiels die Hypothese stützt, gilt hier nur in sehr eingeschränktem Sinne. Solange nicht alle möglichen Eingaben und Zustände durchgetestet wurden, bleibt jeder Bereich eine potenzielle Fehlerquelle. Und alle möglichen Eingaben und Zustände durchzutesten, ist in praktisch jedem nicht-trivialen Programm aussichtslos.
Die Zustandsraumexplosion
Schon einfache Funktionen haben mehr mögliche Eingabekombinationen, als sich vollständig durchprobieren lassen. Eine Funktion, die drei Ganzzahlen mit jeweils 32 Bit entgegennimmt, hat 2 hoch 96 mögliche Eingabekombinationen, ungefähr 80 Quadrillionen. Selbst bei einer Milliarde Tests pro Sekunde würde es mehr als zwei Billionen Jahre dauern, alle Kombinationen zu testen, ein Vielfaches des Alters des Universums. Und das ist eine triviale Funktion. Sobald Strings, Listen oder beliebige Objekte als Eingaben hinzukommen, wird der Eingaberaum buchstäblich unendlich groß.
Dieses Phänomen wird als Zustandsraumexplosion bezeichnet. Bei zustandsbehafteten Programmen, also fast jedem realen System, kommt eine weitere Dimension hinzu: Das Verhalten hängt nicht nur von der aktuellen Eingabe ab, sondern auch vom internen Zustand, der wiederum durch frühere Eingaben geprägt ist. Eine Webanwendung mit zwanzig Sessions, jeweils mit unterschiedlich gesetzten Variablen, hat einen Zustandsraum, der sich nicht mehr sinnvoll überschauen lässt. Bei verteilten Systemen wie im Chord-Beispiel multipliziert sich der Zustandsraum eines einzelnen Knotens mit der Anzahl der Knoten und ihrer möglichen Kommunikationsmuster. Was beim Einzelknoten noch handhabbar wirkte, wird im Verbund unbeherrschbar.

Die auf Developer Experience (DX) und Platform Engineering spezialisierte CLC-Konferenz findet vom 11. bis 12. November 2026 in Mannheim statt. Sie legt den Fokus darauf, wie Agentic AI die Arbeit von Developern, Software-Architekten, DevOps- und Platform Engineers verändert und sich digitale Souveränität nachhaltig erreichen lässt.
Jetzt Tickets zum Frühbucherpreis sichern.
Die übliche Antwort auf dieses Problem heißt Äquivalenzklassen. Statt jede Eingabe einzeln zu testen, werden Eingaben in Klassen zusammengefasst, die aus Sicht des Programms gleich behandelt werden sollten. Statt alle ganzen Zahlen zu testen, reichen ein typischer positiver Wert, ein typischer negativer Wert, die Null und die Grenzwerte. Das ist pragmatisch und meistens ausreichend, aber es setzt voraus, dass die Klassenbildung korrekt ist, dass also alle relevanten Fälle erkannt wurden. Genau hier liegen viele Bugs verborgen: in den Fällen, deren Eigenständigkeit niemand erkannt hat.
Das Orakel-Problem
Selbst wenn sich der Eingaberaum vollständig durchprobieren ließe, bliebe eine zweite, oft übersehene Frage. Woher weiß ein Test, was die richtige Ausgabe ist? Diese Frage ist als Orakel-Problem in der Testtheorie bekannt. Ein Test besteht im Kern aus zwei Teilen: einer Eingabe und einer Erwartung an die Ausgabe. Die Eingabe lässt sich automatisch generieren, die Erwartung im Allgemeinen nicht.
Bei mathematischen Funktionen mag das Orakel offensichtlich sein. Eine Sortierfunktion soll eine sortierte Liste liefern, das lässt sich prüfen. Eine Quadratwurzel soll eine Zahl liefern, die mit sich selbst multipliziert die Eingabe ergibt. Bei realen Anwendungen ist die Lage komplizierter. Was ist die richtige Antwort einer Suchmaschine auf eine bestimmte Anfrage? Was ist die richtige Reihenfolge in einem Newsfeed? Was ist die richtige Empfehlung in einem Webshop? In all diesen Fällen gibt es kein objektives Orakel, sondern nur Heuristiken, Spezifikationen, Geschäftsregeln, manchmal nur menschliches Urteil. Tests können diese Orakel approximieren, aber sie sind selbst eine Quelle möglicher Fehler.
Im Chord-Beispiel zeigt sich das Orakel-Problem in einer besonders perfiden Variante. Die einzelnen Funktionen jedes Knotens hatten klar definierte Erwartungen, und die Tests prüften diese Erwartungen korrekt. Aber das richtige Verhalten des Systems beim Ausfall eines Knotens war nirgendwo als Orakel formuliert, weil niemand explizit darüber nachgedacht hatte. Tests können nur prüfen, was jemand vorher als Erwartung formuliert hat. Was niemand erwartet, kann auch niemand testen.
Das Halteproblem in neuer Rolle
Aus Teil 2 dieser Serie ist das Halteproblem bereits bekannt: Alan Turing hat 1936 bewiesen, dass es kein allgemeines Verfahren geben kann, das für ein beliebiges Programm und eine beliebige Eingabe entscheidet, ob das Programm terminiert oder in eine Endlosschleife gerät. Im Kontext des Testens taucht dieses Resultat in einer subtileren Variante auf.
Wenn ein Test einen Code aufruft, dann muss die Test-Infrastruktur entscheiden, wann sie diesen Aufruf abbricht. Bleibt das Programm einfach hängen, läuft auch der Test ewig. Das übliche Mittel ist ein Timeout: Nach einer bestimmten Zeit gilt der Test als fehlgeschlagen. Dieses Vorgehen ist eine Heuristik, kein Beweis. Ein Test mit Timeout kann nicht zwischen einem Programm, das in einer Endlosschleife steckt, und einem Programm, das einfach nur sehr lange braucht, unterscheiden. Beide werden gleichermaßen als Fehler gemeldet, oder beide als Erfolg, je nach Konfiguration. Die fundamentale Unentscheidbarkeit des Halteproblems schlägt hier auf eine ganz praktische Frage durch: Wie lange darf ein Test laufen, bevor er als gescheitert gilt?
Eine verwandte Konsequenz betrifft die Generierung von Tests. Werkzeuge, die Tests automatisch erzeugen wollen, müssen entscheiden, welche Pfade durch den Code überhaupt erreichbar sind. Diese Entscheidung ist im Allgemeinen ebenfalls unentscheidbar, was aus Rice’s Theorem folgt, das bereits in Teil 2 eine Rolle spielte. Praktische Werkzeuge nähern sich dem mit Heuristiken an, aber sie können nicht garantieren, alle relevanten Pfade zu finden.
Was die Praxis daraus macht
Trotz dieser fundamentalen Grenzen gibt es Verfahren, die deutlich mehr leisten als klassisches Beispiel-basiertes Unit-Testing. Sie verfolgen unterschiedliche Strategien, um den Eingaberaum strukturiert zu erschließen, auch wenn er sich nicht vollständig durchprobieren lässt.
Beim Property-based Testing werden nicht einzelne Beispiele getestet, sondern Eigenschaften, die für alle Eingaben gelten sollen. Statt zu prüfen, dass das Sortieren der Liste [3, 1, 2] das Ergebnis [1, 2, 3] liefert, wird die Eigenschaft formuliert, dass das Ergebnis einer Sortierung dieselben Elemente enthält wie die Eingabe und in aufsteigender Reihenfolge vorliegt. Das System generiert dann automatisch eine große Zahl zufälliger Eingaben und prüft die Eigenschaft. Wird ein Gegenbeispiel gefunden, versucht das Werkzeug, es zu einem minimalen Fall zu reduzieren, der den Bug noch zeigt. Property-based Testing wurde mit dem Werkzeug QuickCheck für Haskell populär und ist inzwischen für viele Programmiersprachen verfügbar.
Beim Fuzzing wird das System mit zufällig generierten oder mutierten Eingaben bombardiert, oft mit dem Ziel, Abstürze, Sicherheitslücken oder unerwartete Zustände zu finden. Moderne Fuzzer nutzen Coverage-Informationen, um Eingaben zu bevorzugen, die neue Codepfade auslösen, und arbeiten sich auf dem Weg systematisch in den Eingaberaum vor. Fuzzing hat zahlreiche schwerwiegende Bugs in weitverbreiteten Bibliotheken aufgedeckt, etwa in Browsern, Bildverarbeitungs-Bibliotheken und Kryptographie-Implementierungen.
Beim Mutation Testing schließlich werden nicht die Programme, sondern die Tests selbst getestet. Das Werkzeug nimmt kleine Änderungen am Quellcode vor, etwa wird ein Plus zu einem Minus, ein Größer-als zu einem Kleiner-als, eine Bedingung negiert. Anschließend werden die Tests gegen den derart mutierten Code laufen gelassen. Wenn die Tests die Mutation nicht bemerken, ist das ein Hinweis darauf, dass die Tests die betroffene Stelle nicht wirklich prüfen, sondern den Code nur durchlaufen lassen. Mutation Testing schließt damit genau die Lücke, die Coverage offenlässt: Es misst nicht, ob Code ausgeführt wurde, sondern ob seine Korrektheit tatsächlich überprüft wird.
All diese Verfahren erweitern die Möglichkeiten des Testens erheblich. Aber keines von ihnen löst das fundamentale Problem. Property-based Testing kann unendlich viele Eingaben nicht in endlicher Zeit prüfen, sondern nur stichprobenartig. Fuzzing findet viele Bugs, aber nicht alle. Mutation Testing prüft die Tests, aber das Orakel-Problem bleibt. Die Verfahren machen Tests besser, aber sie heben die Grenzen nicht auf.
Vertrauen statt Beweis
Der Weg vom Chord-Beispiel über Code Coverage, Dijkstras Bonmot, Zustandsraumexplosion, Orakel-Problem und Halteproblem zeigt eine durchgängige Linie. Tests können niemals beweisen, dass ein Programm korrekt ist. Sie können nur Vertrauen aufbauen. Dieses Vertrauen ist nicht wertlos, im Gegenteil: Es ist der pragmatische Ausweg aus einer Reihe theoretischer Unmöglichkeiten. Aber es darf nicht mit einem Beweis verwechselt werden.
Was diese Einsicht in der Praxis bedeutet, lässt sich in wenige Punkte fassen. Tests sollten so geschrieben werden, dass sie aussagekräftige Eigenschaften prüfen, nicht nur Code zur Ausführung bringen. Eine hohe Coverage ist ein notwendiges, aber kein hinreichendes Kriterium für gute Tests. Verschiedene Testarten ergänzen sich, denn Unit-Tests, Integrationstests, End-to-End-Tests, Property-based Tests und Fuzzing decken jeweils unterschiedliche Klassen von Fehlern ab. Und vor allem: Wenn ein System produktiv geht, sollte das Vertrauen in die Tests durch Mechanismen ergänzt werden, die mit den verbleibenden Fehlern umgehen können. Monitoring, Observability, Circuit Breaker, Rollback-Strategien und Chaos Engineering sind keine Eingeständnisse mangelnder Testqualität, sondern Ausdruck der Einsicht, dass Tests allein nicht reichen können.
Im Chord-Beispiel war die Lehre genau diese: Die Tests selbst waren nicht falsch, sie hatten nur einen anderen Geltungsbereich als das Team angenommen hatte. Erst der Betrieb mit mehreren Knoten und gezielt herbeigeführten Ausfällen brachte das Problem zutage. Das ist kein Versagen des Testens, sondern eine Konsequenz seiner prinzipiellen Grenzen.
Eine andere Klasse von Grenzen, die in dieser Serie noch nicht behandelt wurde, betrifft nicht die Entscheidbarkeit, sondern die Effizienz. Es gibt Probleme, die theoretisch lösbar sind, für die aber kein praktikabler Algorithmus bekannt ist. Im nächsten Teil geht es um eine der wichtigsten offenen Fragen der theoretischen Informatik, die zugleich eine sehr konkrete Bedeutung für den Alltag von Entwicklerinnen und Entwicklern hat: P versus NP.
Lesen Sie auch
(mro)
Entwicklung & Code
Wenn Agenten nur noch mit Agenten reden, reden Menschen aneinander vorbei
Letztens hat einer meiner langjährigen Kunden ein Issue in einem unserer GitHub-Projekte eröffnet. Saubere Überschrift, darunter ein Befund: fehlende Alt-Texte auf Bildern, Verweis auf WCAG 1.1.1 zur Accessibility, dazu eine korrekt analysierte Anomalie in unserem Datenbankschema und schließlich das Angebot, für rund 1.500 Bilder inhaltlich geprüfte Alt-Texte zu liefern. Fachlich gab es nichts zu meckern. Trotzdem stimmte etwas nicht. Kein Gruß, kein persönliches Wort, und der Text erklärte mir mein eigenes Projekt so geduldig, als wäre ich gestern neu dazugestoßen. So schreibt nicht der Mensch, mit dem ich seit gut zehn Jahren zusammenarbeite. So schreibt sein KI-Agent.
Weiterlesen nach der Anzeige

Stefan Wintermeyer ist Consultant und Trainer für Software- und Systemarchitektur. Fokus: Phoenix, Ruby on Rails, Web-Perf, Asterisk/VoIP, KI und Agentic Programming, effektive Workflows und nutzerseitige Verhaltensmuster.
Geantwortet habe ich natürlich auch nicht selbst. Claude Code hat das Issue analysiert, den Fix gebaut, einen ausführlichen Kommentar hinterlassen und das Issue geschlossen. Höflich, gründlich, mit einem Gegenvorschlag zum Datenformat des Kunden. Als ich die Antwort später las, blieb ich an einem Gedanken hängen: Hier sprechen nicht mein Kunde und ich miteinander. Hier reden zwei Maschinen, und wir Menschen lesen bestenfalls noch Protokoll.
Mein Agent ist höflicher als ich
Seit über einem Jahr läuft meine GitHub-Kommunikation fast vollständig über einen Agenten. Nur selten tippe ich noch selbst einen Kommentar. Die Bilanz fällt eindeutig aus: Ich bin deutlich produktiver geworden. Und, das gebe ich ungern zu, zu den meisten Menschen auch freundlicher. Amerikaner sagen mir seit Jahren, dass ich nichts sugarcoate: Deutsche Direktheit wirkt im Englischen schnell ruppig, meine erst recht. Umgekehrt überlese ich als Deutscher manche höflich verpackte Botschaft komplett. Schreibt ein amerikanischer Reviewer unter meinen Pull Request „You might want to consider adding a test here“, lese ich einen unverbindlichen Vorschlag, über den ich bei Gelegenheit mal nachdenken kann. Gemeint ist: Ohne Test wird das hier nicht gemergt. Und sein „Interesting approach!“ ist kein Lob, sondern höflich verpackte Skepsis. Mein Agent erkennt diese sprachlichen Codes in beide Richtungen.
Dazu kommt ein Luxus, den mir kein Mensch bieten könnte: Ich kann mit dem Agenten so reden, wie ich mich gerade fühle. Super unfreundlich, wenn mich etwas nervt. Super happy, wenn ein Fix endlich sitzt. Dem Agenten ist das egal, er ist ja nie beleidigt. Er kümmert sich darum, dass die Botschaft ankommt, ohne dass sich am anderen Ende jemand auf den Fuß getreten fühlt.
Für meine Open-Source-Projekte ist er ein Lebensretter. Eines davon ist vutuv, eine quelloffene Alternative zu LinkedIn, die ich als einziger Entwickler betreue. An manchen Tagen kommen dort dutzende Feature Requests herein, dazu hin und wieder ein paar Bug-Meldungen. Als Einzelkämpfer ist man damit schnell überfordert, jedenfalls ohne einen Agenten an der Seite. Auf ausführliche Antworten zu Issues hatte ich früher oft weder Zeit noch Lust. Verdient hätten die Menschen dahinter sie trotzdem. Wer sich die Mühe macht, einen Fehler ordentlich zu melden, bekommt jetzt eine ordentliche Antwort und am Ende ein ehrliches Dankeschön. Und diese Antwort hat mehr Substanz, als ich je hineintippen würde: Der Agent weiß haarklein, wie er und ich gemeinsam den Fehler behoben haben, wie langwierig und kompliziert das war und welche Fallstricke unterwegs lauerten. Er war ja dabei. Mir fehlt die Zeit, das alles aufzuschreiben, ihm nicht. Win-win für alle Beteiligten.
Weiterlesen nach der Anzeige
Damit das nach mir klingt und nicht nach Pressestelle, habe ich dem Agenten eigene Tonfall-Regeln geschrieben. In meiner CLAUDE.md, der zentralen Konfigurationsdatei, stehen Anweisungen wie: Schreibe in der ersten Person, warm und freundlich. Nenne kurz den Grund, dann hör auf, kein Füllmaterial. Wenn ich etwas ablehne, nie als plattes Nein, sondern freundlich und mit dem Warum. Und wenn der Thread auf Deutsch geführt wird, antworte auf Deutsch.
Es gibt auch eine weniger vorzeigbare Seite. Manche Open-Source-Diskussionen fressen Zeit, ohne je irgendwo anzukommen. Dann sage ich Claude Code: „Mach hier bitte einen Deckel drauf. Die Sache ist entschieden, ich mag das nicht ewig weiterdiskutieren.“ Der Agent bleibt daraufhin freundlich beim Nein und schließt im Zweifel das Issue. Ich delegiere in diesem Moment etwas anderes als Arbeit: nämlich einen Konflikt. Ob die Betroffenen ahnen, dass eine Maschine den Schlussstrich gezogen hat? Vermutlich nicht. Wie es sich umgekehrt anfühlt, wenn ein Maintainer meine sorgfältig geschriebene Bug-Meldung von einem Agenten abmoderieren lässt, weiß ich dagegen nicht. Bisher war ich immer der, der den Deckel in der Hand hält.
Das Minenfeld bekommt eine zweite Ebene
Textkommunikation war schon immer ein Minenfeld. Ironie kommt nicht an, Knappheit wird als Kälte gelesen, ein fehlendes Smiley verschiebt die Bedeutung eines ganzen Absatzes. Bisher half die Erfahrung mit dem Absender: Nach ein paar Jahren weiß ich, wie mein Kunde klingt, wenn er sich freut, und wie, wenn es brennt. Diese Kalibrierung läuft jetzt ins Leere. Der Ton, den ich lese, stammt von einem Werkzeug und dessen Konfiguration. Wenn eine Seite das nicht weiß, entstehen Missverständnisse zwischen zwei Personen, die an dem Gespräch gar nicht teilgenommen haben.
Wie selten bei agentengeschriebenen Pull Requests überhaupt noch ein Mensch draufschaut, ist inzwischen ausgezählt (siehe Textbox „Studienlage: Agenten prüfen Agenten“). Nicht selten liegt morgens eine E-Mail in meinem Postfach, der ich ansehe, dass ein Agent sie geschrieben hat. Marketingleute haben diese Werkzeuge längst genauso für sich entdeckt wie wir Entwickler. Manche Menschen kommunizieren sogar ausschließlich so: Sie lassen Systeme wie OpenClaw ihre E-Mails und Tweets vollautomatisch beantworten. Wer ihnen schreibt, bekommt eine Antwort von jemandem, der die Nachricht nie gelesen hat.
Zwei Übersichtsarbeiten von 2024 zu KI-Agenten in der Softwareentwicklung ordnen das Feld. Junwei Liu und Kollegen werten Arbeiten zu LLM-Agenten in der Softwareentwicklung aus – inzwischen sind es 124 – und behandeln Multi-Agenten-Systeme sowie die Zusammenarbeit von Mensch und Agent in eigenen Kapiteln. Yanlin Wang und Kollegen legen daneben eine Architektur aus Wahrnehmung, Gedächtnis und Aktion vor.
Spannender als die Überblicke sind die Messungen, und die gibt es seit 2026. Grundlage ist meist AIDev, ein Datensatz von Hao Li, Haoxiang Zhang und Ahmed E. Hassan: 932.791 Pull Requests, die fünf Agenten (Codex, Devin, Copilot, Cursor und Claude Code) in 116.211 GitHub-Repositories eröffnet haben, Stand August 2025.
Darauf aufbauend hat eine polnische Gruppe um Kacper Duma jene 33.596 agentengeschriebene Pull Requests untersucht, die in Projekten mit mehr als 100 Sternen liegen. 61,38 Prozent davon bekommen überhaupt kein Review. Von denen, die eines bekommen, werden 58,77 Prozent ausschließlich von anderen Agenten begutachtet und nur 10,14 Prozent ausschließlich von Menschen. Zusammengerechnet durchlaufen 84 Prozent aller agentengeschriebenen Pull Requests entweder kein Review oder nur eines, an dem kein Mensch beteiligt war. Von sämtlichen Review-Kommentaren stammen 71,58 Prozent von Agenten.
Wie viel von alldem überhaupt sichtbar ist, steht auf einem anderen Blatt. Arsham Khosravani und Audris Mockus haben 180 Millionen Repositories nach Agentenspuren durchsucht und kommen auf über 320.000 Commits pro Monat. Wer sich auf die offiziellen Bot-Accounts verlässt, übersieht den Großteil davon: Bei Claude Code fanden die beiden 850.157 Commits, von denen die Bot-Suche nur 28.154 erfasste, also 3,3 Prozent. Der Rest läuft unter menschlichem Namen.
Eine Ebene bleibt unvermessen: Wie oft ein Mensch am Ende gar nicht mehr mitliest, weiß niemand. Die Zahlen erfassen Pull Requests und Reviews, nicht die Frage, ob der Kunde sein eigenes Issue je gelesen hat.
Zum zweiten Thema dieses Artikels, der Offenlegung, liefert die Organisationsforschung die Zahlen. Die im Text zitierte Arbeit von Oliver Schilke und Martin Reimann stützt sich auf dreizehn Experimente mit über 5.000 Teilnehmern, quer durch Rollen und Branchen, vom Professor über den Analysten bis zum Investmentfonds. Den Vertrauensverlust in Texte, die als KI-generiert offengelegt sind, erklären die beiden mit geringerer wahrgenommener Legitimität, nicht mit Zweifeln an der Qualität des Ergebnisses. Bei Befragten mit positiver Technikeinstellung fällt er schwächer aus, verschwindet aber nicht.
Meinem Kunden habe ich nach dem Issue von heute Morgen eine SMS geschrieben und das Thema offen angesprochen. Das hat die Sache ohne Verstimmung entschärft. Wir wissen jetzt beide, dass unsere Agenten miteinander reden. Unausgesprochen wäre die Kommunikation ein schleichendes Gift geworden. Er hätte sich irgendwann gefragt, warum ich so distanziert schreibe. Ich hätte mich gefragt, warum er mich neuerdings für begriffsstutzig hält.
Brandgefährlich ist das Ganze ausgerechnet bei Menschen wie ihm: bei denen, mit denen ich seit Jahren eng zusammenarbeite, ob im Kundenprojekt oder bei Open Source. Sie kennen mich persönlich, und genau deshalb richtet ein fremder Ton den größten Schaden an. Da steht ein Text, der schlicht nicht ich bin, und der Empfänger kennt mich gut genug, um genau das zu spüren. In gewachsenen Arbeitsbeziehungen kann er die Kommunikation zerreißen.
Entwicklung & Code
.NET 11.0 Preview 7 bringt viele Neuigkeiten und Breaking Changes
.NET 11.0 Preview 7 gibt es kostenfrei zum Download bei Microsoft. Dabei handelt es sich um die letzte Preview-Version vor dem ersten Release Candidate. Parallel dazu ist ein Update für Visual Studio 2026 Insiders auf die Version 12106.202 erschienen.
Weiterlesen nach der Anzeige

Dr. Holger Schwichtenberg hat Fachbücher zu .NET 10.0, C# 14.0, Blazor 10.0 und Entity Framework Core 10.0 veröffentlicht. Er arbeitet als Berater und Trainer bei www.IT-Visions.de.

Installation des .NET 11.0 SDK in der Version Preview 7 (Abb. 1)
Mehr Schleifenkontrolle: break und continue mit Bezeichnern
Die Sprachkonstrukte break zum Abbrechen und continue zum vorzeitigen Fortsetzen von Schleifen gibt es in den meisten Programmiersprachen, so auch in C# seit der ersten Version. Nun ergänzt Microsoft aber eine bislang in C# fehlende Option: Entwicklerinnen und Entwickler können hinter break und continue jeweils Namen angeben, die sich auf äußere Schleifen beziehen. Damit kann der Compiler nicht nur die jeweils direkt umgebende Schleife, sondern auch übergeordnete Schleifen abbrechen oder vorzeitig fortsetzen. Ein solches Label kann man bei den Schleifenkonstrukten for, foreach, while und do sowie bei der Verzweigung mit switch einsetzen.
(Bild: King / stock.adobe.com)

Das ist neu in .NET 11.0: Dr. Holger Schwichtenberg und weitere Experten präsentieren am 17. November 2026 auf der Online-Konferenz betterCode() .NET 11.0 die Änderungen für Entwicklerinnen und Entwickler. Tickets zum Frühbucherpreis sind im Online-Shop verfügbar.
Das folgende Listing zeigt dazu ein Beispiel, in dem vermieden wird, dass die drei Schleifenebenen jeweils einzeln abgebrochen werden müssen, wenn ein passender Regalplatz gefunden wurde.
public void Run()
{
List Gaenge = ["A", "B", "C", "D", "E"];
// bool sackgasse = false; // --> nicht notwendig dank continue mit Label
// bool gefunden = false; --> nicht notwendig dank break mit Label
regalgangSchleife:
foreach (var regalgang in Gaenge)
{
for (int regal = 1; regal <= 20; regal++)
{
for (int platz = 1; platz <= 50; platz++)
{
// Sackgasse: In einigen Regalen gibt es Sackgassen
if (regal % 2 == 1 && platz % 13 == 0)
{
CUI.Warning($"Sackgasse: Gang={regalgang}/Regal={regal}/Platz={platz}");
// sackgasse = true; --> nicht notwendig dank continue mit Label
continue regalgangSchleife;
}
CUI.Print($"Prüfe Gang={regalgang}/Regal={regal}/Platz={platz}");
bool istFrei = IstFrei(regalgang, regal, platz);
bool istGrossGenug = IstGrossGenug(regalgang, regal, platz);
if (istFrei && istGrossGenug)
{
CUI.Success($"Gefunden: Gang={regalgang}/Regal={regal}/Platz={platz}");
// gefunden = true; --> nicht notwendig dank break mit Label
break regalgangSchleife;
}
}
// if (sackgasse) continue; --> nicht notwendig dank continue mit Label
// if (gefunden) break; --> nicht notwendig dank break mit Label
}
// if (sackgasse) continue; --> nicht notwendig dank continue mit Label
// if (gefunden) break; --> nicht notwendig dank break mit Label
}
}
Listing 1: Einsatz von Labels bei break und continue
Weiterlesen nach der Anzeige

Der Compiler prüft, ob sich der hinter break oder continue angegebene Bezeichner auf eine Schleife oder ein switch bezieht (Abb. 2).
Automatisches Pausieren für Blazor-Server-Anwendungen
Blazor Server ist eine Variante von Blazor, bei der C#-Programmcode auf dem Webserver läuft und den Webbrowser über eine Circuit genannte Websocket-Verbindung aktualisiert, sodass für Benutzer das Erlebnis einer Single-Page-Webanwendung entsteht. Eine Herausforderung ist bei Blazor Server stets der Ressourcenverbrauch auf dem Webserver, denn alle angeschlossenen Webbrowser teilen sich Prozessorleistung und RAM des Webservers.
Seit .NET 10.0 kann man jeden einzelnen Circuit per JavaScript-Code pausieren via Blazor.pauseCircuit() sowie den Zustand im RAM oder einer Datenbank persistieren und später wiedererwecken via Blazor.resumeCircuit(), um Ressourcen auf dem Webserver zu sparen, wenn ein Benutzer die Browserregisterkarte mit der Blazor-Anwendung verlässt. Seit .NET 11.0 Preview 4 ist das Pausieren von Circuits auch per C#-Code via RequestCircuitPauseAsync() möglich.
Die Einführung von RequestCircuitPauseAsync() war die Vorbereitung für das automatische Pausieren, das es nun in Preview 7 gibt. Entwicklerinnen und Entwickler können nun im Startcode einer Blazor-Server-Anwendung einstellen, dass die Anwendung pausiert, solange die Browserregisterkarte verdeckt ist. Dafür ist ein eigenes NuGet-Paket mit Namen Microsoft.AspNetCore.Components.Server.AutoPause zu ergänzen:

Das NuGet-Paket Microsoft.AspNetCore.Components.Server.AutoPause (Abb. 3)
und der Startcode ist um den Aufruf AddAutoPause() zu erweitern:
app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.WithBrowserOptions(options =>
{
options.AddAutoPause(pause =>
{
pause.HiddenDelay = TimeSpan.FromSeconds(60); // Standard: 2 Minuten
});
});
Im Standard geschieht das Pausieren nach zwei Minuten. Diese Zeit können Entwicklerinnen und Entwickler mit HiddenDelay ändern. Blazor Server hält sich aber nicht akribisch an die definierte Zeitspanne. So wird die Zeit verlängert, wenn ein Eingabesteuerelement neue Werte enthält, wenn die Seite Audio- oder Videoinhalte mit Ton abspielt oder wenn ein JavaScript-Aufruf läuft.

Diese Blazor-Server-Anwendung war für einige Sekunden im Schlaf. Der Benutzer sieht beim Rückwechseln der Registerkarte kurz den Inhalt von ReconnectModal.razor, dann arbeitet die Anwendung weiter (Abb. 4).
Zu beachten ist, dass sich eine Komponente beim Schlafenlegen nur die Werte der mit [PersistentState] annotierten Eigenschaften oder manuell im Persistent Component State gespeicherten Objekte merkt.
CacheView für Blazor Static SSR
Bei dem in .NET 8.0 eingeführten statischen serverseitigen Rendering (Static SSR), das man als Ablösung für ASP.NET Core MVC und ASP.NET Core Razor Pages verstehen kann, schließt Microsoft in .NET 11.0 Preview 7 eine weitere Lücke: Analog zu dem Tag Helper in den älteren Webframeworks gibt es jetzt in Blazor eine neue eingebaute Komponente . Diese Komponente merkt sich den gerenderten Inhalt für eine bestimmte Zeit und rendert den Inhalt erst nach dem Ablauf erneut. Die Zeitspanne können Entwicklerinnen und Entwickler flexibel durch
ExpiresAfter: Zeitspanne seit dem ersten ZugriffExpiresOn: fester Zeitpunkt oderExpiresSliding: Zeitspanne seit dem letzten Zugriff
angeben.
Mit VaryByRoute und VaryByQuery sowie VaryByUser und VaryByCulture lässt sich festlegen, dass es abweichende Zwischenspeicher für verschiedene Parameter in URL und QueryString beziehungsweise Benutzer und Sprachen geben soll.
In zwei Fällen funktioniert die -Komponente nicht:
- wenn die HTTP-Anfrage ein anderes Verb als
GETverwendet oder - wenn Stream Rendering aktiv ist, mit
@attribute [StreamRendering].
Zudem lässt sich das Zwischenspeichern mit der Eigenschaft enabled="false" manuell deaktivieren.
Die von Microsoft gelieferte Razor-Komponente funktioniert nicht innerhalb eines -Elements:
"InvalidOperationException: Component 'Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize`1[[System.DateTime' cannot be used inside a CacheView because its output depends on per-request state ([CacheBehavior(CacheBehavior.Throw)]) that cannot be safely cached and replayed. To fix this, move the component outside the CacheView."
Die Komponente funktioniert nur mit VaryByUser="true" und das -Tabellensteuerelement nur mit VaryByQuery.
Selbsterstellte Komponenten können mit @attribute [CacheBehavior(CacheBehavior.Throw)] angeben, dass sie nicht im funktionieren wollen. Mit @attribute [CacheBehavior(CacheBehavior.Rerender)] werden Komponenten trotz immer neu gerendert. Das geschieht auch mit den eingebauten Komponenten und .
Zwischenspeicher ist im Standard der RAM des Webservers. Durch Einsatz des NuGet-Pakets Microsoft.Extensions.Caching.Hybrid sind auch andere Speicher (etwa Redis, Datenbankmanagementsystem) als Zwischenspeicher möglich.
@page "/Cache"
@page "/Cache/{URLID:int}"
@code
{
[Parameter]
[SupplyParameterFromQuery]
public int URLID { get; set; }
[Parameter]
[SupplyParameterFromQuery]
public int QueryID { get; set; }
}
URLID: @URLID
QueryID: @QueryID
zuletzt aktualisiert am @DateTime.Now
/1
/2
/3
/4
/5
QueryID=1
QueryID=2
QueryID=3
QueryID=4
QueryID=5
Listing 2: Beispiel für den Einsatz von
Neue Analyzer schützen vor Fehlern
Fünf neue in Blazor eingebaute Analyzer warnen Entwicklerinnen und Entwickler vor falschem Programmcode:
- BL0012 warnt, wenn ein Aufruf von
StateHasChanged()nicht notwendig ist, zum Beispiel inOnInitialized(),OnInitializedAsync()und Ereignisbehandlungen. - Wenn
GetAuthenticationStateAsync()aufgerufen wird, ohne das EreignisAuthenticationStateChanged()zu abonnieren, warnt BL0013, weil dadurch eine Komponente mit einem veralteten Authentifizierungsstatus arbeiten kann. - BL0014 warnt, wenn eine Zählvariable einer
for-Schleife von einer Closure oder einem Render-Fragment erfasst wird. Dadurch können alle Callbacks oder Fragmente auf den Wert der letzten Iteration verweisen. - Eine mit
[JSInvokable]markierte Methode ist nicht öffentlich. Sie lässt sich zwar kompilieren, kann aber nicht aus JavaScript aufgerufen werden. Der Analyzer BL0015 bietet einen Code-Fix an, der die Methodepublicmacht. - Wenn
InvokeAsync()oderInvokeVoidAsync()außerhalb einestry-catch-Blocks aufgerufen wird, warnt BL0016. Bei einer unterbrochenen Blazor-Verbindung oder einer JavaScript-Ausnahme kann dadurch die Komponente beziehungsweise der Circuit beendet werden.
Union Types als Parameter
Blazor-Komponenten sollen laut Release Notes nun auch mit Union Types, die in Preview 3 eingeführt wurden, als Parameter reibungslos funktionieren. Im Schnelltest mit dieser Komponente
@code {
public union ContentUnion(string, RenderFragment);
[Parameter]
public ContentUnion? Header { get; set; }
[Parameter]
public ContentUnion? Content { get; set; }
}
@Header.Value
@Content.Value
Listing 3: Message.razor
und diesem Aufruf
@DateTime.Now.ToShortDateString()
Dies ist fett und das ist kursiv!
gab es jedoch stets den Laufzeitfehler
InvalidOperationException: Object of type 'Message' does not have a property matching the name 'ChildContent'.
Die Komponente Message.razor funktioniert hingegen reibungslos, wenn man die Parameter auf RenderFragment statt auf ContentUnion typisiert.
Zahlreiche Breaking Changes gegenüber den vorherigen Preview-Versionen
Wer schon Blazor-Anwendungen mit in vorherigen Preview-Versionen eingeführten Validierungs-Features nutzt – wie der clientseitigen Validierung, vereinfachten Lokalisierung von Validierungs-Fehlermeldungen und der asynchronen Validierung –, muss in Preview 7 vieles am Programmcode ändern, denn Microsoft hat die APIs noch einmal überarbeitet.
Ob beim statischen serverseitigen Rendern die clientseitige Validierung aktiv ist, steuert man nun nicht mehr über EnableClientValidation="true", sondern über DisableClientValidation="false".
Bei der Lokalisierung ist AddValidationLocalization() entfallen; stattdessen gibt man die Ressource (.resx-Datei) mit den lokalisierten Fehlermeldungen nun bei AddValidation() an:
builder.Services.AddValidation(options =>
{
options.LocalizerProvider = (_, factory) => factory.Create(typeof(ValidationMessages));
});
Zum Glück radikal vereinfacht hat Microsoft die asynchrone Validierung, die in Preview 5 eingeführt wurde und sehr komplex war.
Nun in Preview 7 lassen sich asynchrone Validierungsannotationen schreiben, indem man von der Basisklasse AsyncValidationAttribute erbt. Diese asynchronen Validierungsannotationen verwendet man in einem Objektmodell, das man als EditContext für ein Formular verwendet. Dann kann man in der Instanz von EditContext via IsValidationPending() und IsValidationFaulted() den Status der Validierung abfragen.
Die Annotationen [ValidatableType] und [SkipValidation], die es seit .NET 10.0 gibt und seitdem als experimentell galten, erzeugen nun keine solche Warnung mehr.
In .NET 11.0 Preview 6 hatte Microsoft in der Komponente das programmgesteuerte Scrolling zu einer bestimmten Position eingeführt. Dafür gab es die neue Eigenschaft InitialIndex für die Startposition und die neue Methode ScrollToIndexAsync() für eine Bewegung zur Laufzeit. Nun wurde die Eigenschaft in InitialItemIndex und die Methode in ScrollToItemAsync() umbenannt. Das -Steuerelement bietet ab Preview 7 auch InitialItemIndex und ScrollToItemAsync an.
Zahlreiche weitere Breaking Changes in ASP.NET Core 11.0 Preview 7 findet man in den Release Notes.
Entwicklung & Code
LiteLLM-Angriff: Mehr als 2500 Unternehmen betroffen
Bei einem Lieferkettenangriff auf die populäre LiteLLM-Bibliothek im März dieses Jahres haben Angreifer im Python Packages Index (PyPI) kompromittierte Pakete eingeschleust. Diese wurden offenbar von automatisierten Build-Systemen zigtausendfach heruntergeladen und verwendet, bei mehr als 2500 auch namhaften Unternehmen – obwohl die kompromittierten Pakete lediglich 40 Minuten online waren.
Weiterlesen nach der Anzeige
Die Angreifer haben potenziell rund 434.000 automatisierte Entwickler-Build-Pipelines unterwandert, die bei mehr als 2500 Firmen zum Einsatz kommen, führen die IT-Sicherheitsforscher von CloudSEK in einem Blogbeitrag aus. Die Daten deuten darauf hin, dass Angreifer darauf hätten zugreifen können, belegen jedoch nicht, dass sie das auch getan haben und dass alle Zugangsdaten entwendet wurden. Laut den Analysten ist die Bedrohung noch immer aktuell, da auch das FBI im Juli davor gewarnt hat, dass mit der Cybergang TeamPCP verbundene Akteure die gestohlenen Zugangsdaten wahrscheinlich noch lange nach dem eigentlichen Angriff missbrauchen. Daher sollten potenziell Betroffene Sicherheitsmaßnahmen wie Ändern der Zugangsdaten, Schließen der möglichen Schwachstelle und Härtung der Systeme ergreifen, empfehlen die IT-Forscher.
Die Auflistung betroffener Unternehmen führt einige große und namhafte Firmen auf. Airbus, Amazon Web Services, Cisco, Hoffmann-La Roche AG, Salesforce, Samsung, ServiceNow, Siemens, aber auch Epic Games, FedEx, der Rückversicherungsriese Munich Re, die Robert Bosch GmbH, Thales Group, Volkswagen AG oder X Corp tauchen dort auf. Von denen und vielen weiteren haben die Angreifer teils Geheimnisse wie Zugangsdaten ergattert, in allen Fällen liefen jedoch in den automatisierten KI-Build-Systemen die kompromittierten Versionen – zum Teil tausende Male.
Entwendete Objekte
Zu den geklauten Daten zählen laut CloudSEK Cloud-Keys, Repository-Token, SSH-Keys, Kubernetes-Secrets, Zugangsdaten für Paket-Publishing, Umgebungsvariablen und KI-Anbieter-Keys. Das Entfernen der bösartigen Pakete allein reicht daher nicht, da derartige Zugangsdaten noch Wochen oder Monate nutzbar bleiben, sofern sie nicht geändert und Hinweise auf unbefugte Aktivitäten untersucht werden.
Die Infostealer-Malware, die Google „Sandclock“ nennt, lief auf kompromittierten Build-Systemen, weitete ihre Rechte zu root aus und suchte dann nach den Daten. Insbesondere auf KI-Build-Systemen sucht sie nach LLM-API-Keys und Gateway-Konfigurationen. Die Daten haben die Angreifer mit AES-256 und einem fest einprogrammierten RSA-4096-Key verschlüsselt und an Server gesendet, die eine Typosquatting-Domain nutzten. Wenn das Ausschleusen fehlschlägt, legt die Malware ein öffentliches Repository innerhalb des GitHub-Kontos der Opfer an und verfrachtet die Daten dorthin.
CloudSEK gibt in der Analyse noch weitere Tipps und Hinweise für Admins, wie sie eine Kompromittierung aufspüren, eindämmen und wie sie sich vor den Attacken schützen können.
Weiterlesen nach der Anzeige
(dmk)
-
Entwicklung & Codevor 2 WochenKommentar: KI-Verbote in Open-Source-Projekten können KI nicht stoppen
-
UX/UI & Webdesignvor 1 MonatRegional & mit Gefühl: Identity für Klimafonds Baden-Württemberg › PAGE online
-
Entwicklung & Codevor 3 MonatenProxmox Datacenter Manager 1.1 wird Schweizer Taschenmesser für Proxmox-Cluster
-
UX/UI & Webdesignvor 3 WochenVom Scribble zur Sammelkarte: Kreativer KI-Workflow mit Adobe Photoshop, Illustrator und Firefly Boards › PAGE online
-
Apps & Mobile Entwicklungvor 3 MonatenFrüher jubeln als der Nachbar: Vodafone beschleunigt ARD und ZDF zur Fußball-WM im Kabel‑TV
-
Entwicklung & Codevor 3 MonatenKommentar: Das Ende der SaaS-Gelddruckmaschine
-
UX/UI & Webdesignvor 3 MonatenWelches sind die zukünftig wichtigsten »Glückshebel« für Agenturen? › PAGE online
-
Apps & Mobile Entwicklungvor 3 MonatenMSI MPG OLED 322URDX36: Erster Monitor mit Triple Mode und QD‑OLED Gen 5
