Vor fast genau zwei Jahren habe ich bereits einen Artikel darüber geschrieben, wie Testautomatisierung auf Ebene der UI mit Avalonia möglich ist [1]. Der dort beschriebene Ansatz funktioniert, erfordert aber auch etwas Starthilfe in Form von einigen Utility-Klassen und der richtigen Konfiguration von Avalonia und Testframework. Hintergrund ist die Tatsache, dass Avalonia wie andere UI-Frameworks Single-Threaded ist und ähnlich wie WPF mit einem Application Objekt arbeitet. In automatisierten UI-Tests muss man sich somit darum kümmern, die Tests innerhalb des UI-Threads auszuführen, eine Instanz von Application zu teilen und die parallele Ausführung der Tests zu deaktivieren. Mit den Paketen Avalonia.Headless.XUnit oder Avalonia.Headless.NUnit sind diese Schritte aber glücklicherweise nicht mehr erforderlich. In diesem Artikel beschäftigen wir uns mit Avalonia.Headless.XUnit, da ich selber typischerweise mit XUnit arbeite.
Inhaltsverzeichnis
Automatisierter Test mit Avalonia.Headless.XUnit
Schauen wir uns etwas genauer an, was das bedeutet. Im letzten Artikel [1] habe ich bereits auf die Testfälle im Projekt RolandK.AvaloniaExtensions [2] verwiesen. Bei den UI-Tests dort wird im Wesentlichen die Funktionalität in den Basisklassen wie MvvmUserControl getestet. Zum Zeitpunkt des letzten Artikels war dafür noch nachfolgender Code nötig. Die Klasse UnitTestApplication ist dabei eine Hilfsklasse, die sich um die eingangs angesprochenen Themen wie dem UI-Thread kümmert.
[Fact] public Task Attach_MvvmUserControl_to_ViewModel() { return UnitTestApplication.RunInApplicationContextAsync(() => { // Arrange var testMvvmControl = new MvvmUserControl(); var testViewModel = new TestViewModel(); // Act testMvvmControl.DataContext = testViewModel; var testRoot = new TestRoot(testMvvmControl); // Assert Assert.Equal(testMvvmControl, testViewModel.AssociatedView); GC.KeepAlive(testRoot); }); }
Nach der Migration auf Avalonia.Headless.XUnit sieht das Beispiel wie im nächsten Codeausschnitt aus. Die Logik zum Testfall selbst, also die Schritte zu ‚Arrange‘, ‚Act‘ und ‚Assert‘, sind identisch. Außen herum gibt es folgende Unterschiede:
- Wir nutzen [AvaloniaFact] anstelle von [Fact]
- Der Aufruf zur eigenen Hilfsklasse UnitTestApplication fehlt
- Die Testmethode gibt keinen Task mehr zurück
Tatsächlich ist die UnitTestApplication nicht mehr notwendig. Avalonia kümmert sich im Hintergrund selbst darum. Damit Avalonia das tun kann, müssen die Testmethoden für die UI-Tests mit [AvaloniaFact] (anstelle von [Fact]) oder [AvaloniaTheory] (anstelle von [Theory]) markiert werden. Das war es dann auch schon. Im konkreten Beispiel fiel zusätzlich der Task als Rückgabewert raus – das liegt aber lediglich daran, dass für den Testfall selbst keine asynchronen Vorgänge notwendig sind.
[AvaloniaFact] public void Attach_MvvmUserControl_to_ViewModel() { // Arrange var testMvvmControl = new MvvmUserControl(); var testViewModel = new TestViewModel(); // Act testMvvmControl.DataContext = testViewModel; var testRoot = TestRootWindow.CreateAndShow(testMvvmControl); // Assert Assert.Equal(testMvvmControl, testViewModel.AssociatedView); GC.KeepAlive(testRoot); }
Bootstrapping der Application
Eine weitere zu beachtende Kleinigkeit bleibt dann aber doch, wenn man Avalonia.Headless.XUnit wie im Beispiel oben nutzen möchte. Im Test-Projekt muss an einer Stelle definiert werden, welche Application hochgefahren werden soll. Das kann die App.axaml im zu testenden Projekt sein. Alternativ eine eigene, speziell für das Test-Projekt definierte App.axaml. Im Projekt RolandK.AvaloniaExtensions nutze ich letzteres [3]. Die App.axaml inkl. der App.axaml.cs ist hier direkt im Test-Projekt definiert. Zusätzlich dazu sorgt die Klasse TestAppBuilder für das Bootstrapping von Avalonia innerhalb des Test-Projekts. Nachfolgender Codeausschnitt zeigt, wie das im Code aussieht. Zu beachten ist dabei das Attribut AvaloniaTestApplication (Zeile 4). Dieses Attribut wird von Avalonia genutzt, um die Logik für das Bootstrapping der Application zu finden.
using Avalonia; using Avalonia.Headless; [assembly: AvaloniaTestApplication( typeof(RolandK.AvaloniaExtensions.Tests.Util.TestAppBuilder))] namespace RolandK.AvaloniaExtensions.Tests.Util; public class TestAppBuilder { public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>() .UseHeadless(new AvaloniaHeadlessPlatformOptions()); }
Fazit
Avalonia stellt einen einfachen Mechanismus bereit, automatisierte UI-Tests schreiben zu können. Meine persönliche Empfehlung ist dabei, diesen in den eigenen Projekten auch zu nutzen. UI-Tests ermöglichen es, nicht nur einzelne Logik-Bausteine, sondern das Verhalten der Applikation insgesamt zu testen. Mit den hier vorgestellten Möglichkeiten sind wir zwar noch nicht bei End-to-End Tests angekommen, Tests gegen einzelne UserControls sind aber mit überschaubarem Aufwand möglich. Ein Blick in mein Projekt RolandK.AvaloniaExtensions beweist genau das.
Verweise
- Testautomatisierung mit Avalonia
https://www.rolandk.de/wp-posts/2023/03/testautomatisierung-mit-avalonia/ - GitHub Projekt zu RolandK.AvaloniaExtensions
https://github.com/RolandKoenig/RolandK.AvaloniaExtensions - Bootstrapping der Application im Test-Projekt bei RolandK.AvaloniaExtensions
https://github.com/RolandKoenig/RolandK.AvaloniaExtensions/tree/…/src/RolandK.AvaloniaExtensions.Tests/Util
Ebenfalls interessant
- Avalonia UI – Per MarkupExtension auf das Betriebssystem eingehen
https://www.rolandk.de/wp-posts/2025/01/avalonia-ui-per-markupextension-auf-das-betriebssystem-eingehen/ - Cross-Plattform GUI mit C# und Avalonia UI
https://www.rolandk.de/wp-posts/2020/07/cross-platform-gui-mit-c-und-avalonia/ - GPXviewer 2 – Mit Avalonia UI auf mehrere Plattformen
https://www.rolandk.de/wp-posts/2025/01/gpxviewer-2-mit-avalonia-ui-auf-mehrere-plattformen/