Testing Avalonia Applications
Avalonia's headless test platform runs real UI tests in CI in milliseconds with no display server — the most testable desktop stack in .NET.
2 min read · updated 19 September 2026
Avalonia is a cross-platform .NET UI framework, and its testing story is the most interesting in desktop software: a headless platform that runs the real UI stack with no windowing system at all.
Layout runs. Styles apply. Bindings evaluate. Input is dispatched. It takes milliseconds and needs no display server, which means real UI tests run in an ordinary Linux CI container.
Nothing else covered in desktop testing offers this.
#Setting it up
<PackageReference Include="Avalonia.Headless.XUnit" Version="11.*" />
<!-- Add this too if you want screenshots: -->
<PackageReference Include="Avalonia.Skia" Version="11.*" />// TestAppBuilder.cs — one per test assembly
using Avalonia;
using Avalonia.Headless;
[assembly: AvaloniaTestApplication(typeof(TestAppBuilder))]
public static class TestAppBuilder
{
public static AppBuilder BuildAvaloniaApp() => AppBuilder
.Configure<App>()
.UseHeadless(new AvaloniaHeadlessPlatformOptions
{
UseHeadlessDrawing = false // false => Skia renders, screenshots work
});
}#A real UI test
public class CheckoutViewTests
{
[AvaloniaFact]
public void Entering_a_card_enables_the_pay_button()
{
var window = new Window { Content = new CheckoutView { DataContext = new CheckoutViewModel() } };
window.Show();
var pay = window.FindControl<Button>("PayButton")!;
var card = window.FindControl<TextBox>("CardNumber")!;
Assert.False(pay.IsEnabled);
card.Text = "4242424242424242";
Dispatcher.UIThread.RunJobs(); // let bindings and layout settle
Assert.True(pay.IsEnabled);
}
[AvaloniaFact]
public void Clicking_pay_shows_the_confirmation()
{
var window = new Window { Content = new CheckoutView { DataContext = PaidViewModel() } };
window.Show();
// Real input, dispatched through the real input pipeline.
window.KeyPressQwerty(PhysicalKey.Tab, RawInputModifiers.None);
window.MouseDown(new Point(120, 400), MouseButton.Left);
window.MouseUp(new Point(120, 400), MouseButton.Left);
Dispatcher.UIThread.RunJobs();
var confirmation = window.FindControl<TextBlock>("Confirmation")!;
Assert.Equal("Order confirmed", confirmation.Text);
}
}Dispatcher.UIThread.RunJobs() is the Avalonia equivalent of Angular's
detectChanges or Vue's nextTick: it drains the queued layout and binding
work so the assertion sees settled state. Forgetting it is the most common
reason an Avalonia headless test fails confusingly.
#Layout assertions
Because layout genuinely runs, you can assert on it — which is something neither jsdom nor most desktop tooling allows:
[AvaloniaFact]
public void The_total_stays_visible_at_a_narrow_width()
{
var window = new Window { Width = 320, Height = 640, Content = new CheckoutView() };
window.Show();
Dispatcher.UIThread.RunJobs();
var total = window.FindControl<TextBlock>("Total")!;
var bounds = total.Bounds;
Assert.True(bounds.Width > 0 && bounds.Height > 0);
Assert.True(bounds.Right <= window.Width); // not clipped off the edge
}#Screenshots in CI
[AvaloniaFact]
public void Checkout_matches_the_approved_appearance()
{
var window = new Window { Width = 800, Height = 600, Content = new CheckoutView() };
window.Show();
Dispatcher.UIThread.RunJobs();
using var frame = window.CaptureRenderedFrame()!;
frame.Save("artifacts/checkout.png");
// Compare against a committed baseline with your image-diff library of
// choice, and publish the diff as a CI artifact on failure.
}Visual regression for a desktop application, on a Linux runner, with no display server. See screenshots for the general approach and its pitfalls.
#The pyramid still applies
The headless platform is fast enough that it is tempting to test everything through it. Resist a little: a view model is still cheaper to test directly, and most business logic belongs there.
// No UI at all. Microseconds. This is where most of the tests should be.
[Fact]
public void Pay_is_disabled_until_the_card_is_valid()
{
var vm = new CheckoutViewModel(new StubPaymentService());
Assert.False(vm.CanPay);
vm.CardNumber = "4242424242424242";
Assert.True(vm.CanPay);
}MVVM makes this natural, which is the underlying point: an Avalonia application written with a proper view-model layer has most of its logic in plain classes, and the headless tests then cover the part that genuinely needs a UI — bindings, styles, layout, input.
#What headless cannot cover
- Native dialogs — file pickers, print, system message boxes
- Multi-window and window-manager behaviour
- Platform integration: tray icons, notifications, drag and drop from the OS
- Real GPU rendering differences
- Actual per-platform font rendering
Those need a real session on each target platform, and they are the handful of end-to-end tests an Avalonia application should have. Driven by Appium or FlaUI on Windows — see desktop testing.
#Why this matters beyond Avalonia
The interesting thing here is not the framework; it is the demonstration that a UI toolkit can be built so that its full stack runs headlessly and deterministically. Most desktop frameworks cannot, and that single fact accounts for most of the difficulty in desktop testing.
If you are choosing a .NET desktop stack and test automation is a requirement rather than an aspiration, this is a real point in Avalonia's favour.
Common questions
- What is Avalonia's headless test platform?
- A rendering backend that runs the full Avalonia UI stack — layout, styling, input, data binding — with no windowing system. Tests create real controls, dispatch real input and read real layout results, in milliseconds, on a Linux CI runner with no display.
- Do I still need end-to-end tests for an Avalonia app?
- A few, for things the headless platform cannot cover — native dialogs, file pickers, multi-window behaviour, platform integration. But the headless tests cover far more than UI tests usually can, so the end-to-end layer stays genuinely small.
- Can headless tests take screenshots?
- Yes, with the Skia rendering option enabled. That makes visual regression testing possible in CI with no display server, which is unusual for a desktop framework.
Runnable samples for this page
last test results ↗- C# (Avalonia headless)
dotnet/Tests.Avalonia/platforms/avalonia-testing
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Desktop Application TestingAutomating WPF, WinUI, WinForms, Electron and native desktop apps — the accessibility trees, the tooling, and why desktop UI automation is harder than the web.
- Writing Tests in C#The .NET testing stack — xUnit, Moq or NSubstitute, WebApplicationFactory, Testcontainers — and the dependency injection story that makes it the most testable of the four.
- xUnit.netThe .NET runner with a fresh instance per test — Facts, Theories, fixtures, parallelism and the design opinions baked into it.
- Writing Testable CodeThe properties that make code easy to test — pure cores, injected edges, no hidden state — and the specific smells that make it hard.
- Component TestingTesting one deployable in isolation through its real interface, with its own dependencies containerised and everything beyond its boundary stubbed.