Wer testgetrieben entwickelt und somit nur dann einen Produktivcode schreibt, wenn ein Test rot ist, kommt irgendwann an eine Stelle, an der er einen Test schreiben muss, um zu prüfen, ob einasynchronerAufruf auchawaitedwird. Diese Tests sind zwar möglich, allerdings groß, schwer zu lesen und mühsam zu pflegen. Alternativ zu einem Unit-Test lässt sich das auch auf der Ebene des Compilers prüfen. Da beide Wege sinnvoll sind, werde ich in diesem Artikel auch beide aufzeigen. Zuerst den Test und anschließend wie man sich ihn dank des Compilers sparen kann.
Szenario
Zur Veranschaulichung habe ich mir folgendes Szenario überlegt: Wir schreiben eine Anwendung, in der es einen geschützten Bereich gibt. Damit ein User Zugriff auf diesen Bereich bekommt, muss er sich registrieren. Wir speichern daraufhin die Daten des Users und verschicken eine Mail, um die Identität zu bestätigen. Hier gibt es drei Beteiligte: DenUserRegistrator, welcher diesen Prozess abbildet, dasIUserRepository,welches uns das Speichern der User-Daten ermöglicht und denIMessageDispatcher,welcher die Nachricht zur Identitätsprüfung verschickt.
Wichtig in unserem Beispiel ist, dass die Benachrichtigung erst dann erfolgt, wenn die User-Daten gespeichert wurden. Die User-Daten an sich sind für das Beispiel zweitrangig und wurden zur besseren Übersicht weggelassen.
Der Test
Code sagt mehr als 1000 Worte, also:
Wenn wir dasawaitvor_userRepository.CreateNewUser();in Zeile 67 entfernen, wird der Test rot. Damit ist bewiesen, dass wir auch das Richtige testen. Der „Trick“ ist, mithilfe derTaskCompletionSourcedie Kontrolle darüber zu übernehmen, wannCreateNewUser()fertig wird. Nach dem „Act“ können wir so in aller Ruhe prüfen, dass der nächste Aufruf, also_messageDispatcher.DispatchEmailVerificationMessage();noch nicht erfolgt ist. Wer jetzt findet, das sei doch noch vertretbar viel Code, der bedenke, dass pro Aufruf derawaitedwerden soll, ein eigener Task sowie Asserts notwendig werden.
Bei genauem Hinsehen fällt auf, dass der Test zwar einen asynchronen Code aufruft, selbst allerdings wederawaitbenutzt, noch alsasyncmarkiert ist. Bei dieser Art von Test muss das so sein, da er sonst endlos laufen würde. Dafür müsste der beim „Act“ zurückgegebene Task erst nach den ersten Assertscompletedwerden. Da erst beim „Act“awaitedwird, kann dies allerdings nie passieren.
Die Lösung per Compiler
Das fehlendeawaitim Test erzeugte die Compiler-Warnung „CS4014: Async method invocation without await expression“. Diese nutzen wir bei der Compiler-Lösung. Hierzu fügen wir in den Projekteinstellungen pro Projekt und pro Build-Konfiguration die WarnungCS4014in die Liste der Warnungen ein, die als Fehler behandelt werden sollen. Dies führt dazu, dass sich das Projekt nicht kompilieren lässt. Da in TDD „not compiling“ mit einem fehlgeschlagenen Test gleichgesetzt ist, wären wir damit fertig.
Die Einstellung ist je nach IDE an etwas anderer Stelle und ich werde daher hier nicht beschreiben wo. Entscheidend ist, dass die Projektdateien den Knoten<WarningsAsErrors>CS4014</WarningsAsErrors>enthalten. Wenn weitere Warnungen als Fehler behandelt werden sollen, dann natürlich auch diese.
Sollte man wirklich einen asynchronen Aufruf nichtawaitenwollen, so kann man die Überprüfung mit ein wenig Pragma-Code ab- und wieder anschalten:
Das sollte allerdings die Ausnahme bleiben und ist dann auch einen erklärenden Kommentar wert
Thanks to Jannik Weyrich and Jonas Österle.
Wenn dir dieser Beitrag gefallen hat, melde dich jetzt für unseren Digital Letter an! Auf diesem Weg bekommst du regelmäßig unsere spannendsten Blogbeiträge, nützliche Fachartikel und interessante Events rund um Digitalisierung, IoT, New Work und vieles mehr!