Why xUnit is the Best Choice for Avalonia Projects

For Avalonia UI projects, I’d recommend xUnit as your primary testing framework. Here’s why and how to set it up:

xUnit has become the de facto standard in the .NET ecosystem, especially for modern applications. Here’s why it’s perfect for your Avalonia project:

1. Modern and Clean API

[Fact]
public void LoginUser_ShouldUpdateState()
{
    // Much cleaner than NUnit's [Test] attribute
}

[Theory]
[InlineData("john", true)]
[InlineData("", false)]
public void ValidateUsername_WithDifferentInputs(string username, bool expected)
{
    // Parameterized tests are elegant in xUnit
}

2. Better Async Support

xUnit was built with async/await in mind from the ground up:

[Fact]
public async Task LoginAsync_ShouldCallService()
{
    // Native async support - no special attributes needed
    await _loginViewModel.LoginAsync();
    Assert.True(_userStateService.IsUserLoggedIn);
}

3. Excellent IDE Integration

In JetBrains Rider, xUnit has the best support:

  • Better test discovery
  • Cleaner test runner interface
  • Superior debugging experience

Setting Up Your Test Project

Create your test project structure:

# In your solution directory
dotnet new xunit -n YourProject.Tests
dotnet add YourProject.Tests reference YourProject
dotnet sln add YourProject.Tests

Essential Packages for Avalonia Testing

Add these to your YourProject.Tests.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="xunit" Version="2.4.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
    
    <!-- For mocking -->
    <PackageReference Include="Moq" Version="4.20.69" />
    
    <!-- For Avalonia UI testing -->
    <PackageReference Include="Avalonia.Headless" Version="11.0.7" />
    
    <!-- For fluent assertions (optional but recommended) -->
    <PackageReference Include="FluentAssertions" Version="6.12.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\YourProject\YourProject.csproj" />
  </ItemGroup>

</Project>

Real-World Testing Examples for Your State Management

Testing Your State Service

using Xunit;
using FluentAssertions;
using YourProject.Services;

namespace YourProject.Tests.Services;

public class UserStateServiceTests
{
    private readonly UserStateService _userStateService;

    public UserStateServiceTests()
    {
        _userStateService = new UserStateService();
    }

    [Fact]
    public void LoginUser_ShouldUpdateAllProperties()
    {
        // Arrange
        const string username = "testuser";

        // Act
        _userStateService.LoginUser(username);

        // Assert
        _userStateService.IsUserLoggedIn.Should().BeTrue();
        _userStateService.CurrentUserName.Should().Be(username);
        _userStateService.RecentActivities.Should().NotBeEmpty();
        _userStateService.RecentActivities.First().Should().Contain("logged in");
    }

    [Theory]
    [InlineData("john")]
    [InlineData("admin")]
    [InlineData("user123")]
    public void LoginUser_WithDifferentUsernames_ShouldWork(string username)
    {
        // Act
        _userStateService.LoginUser(username);

        // Assert
        _userStateService.CurrentUserName.Should().Be(username);
        _userStateService.IsUserLoggedIn.Should().BeTrue();
    }

    [Fact]
    public void LogoutUser_AfterLogin_ShouldClearState()
    {
        // Arrange
        _userStateService.LoginUser("testuser");

        // Act
        _userStateService.LogoutUser();

        // Assert
        _userStateService.IsUserLoggedIn.Should().BeFalse();
        _userStateService.CurrentUserName.Should().BeEmpty();
    }
}

Testing ViewModels with Mocking

using Xunit;
using Moq;
using FluentAssertions;
using YourProject.ViewModels;
using YourProject.Services;

namespace YourProject.Tests.ViewModels;

public class LoginViewModelTests
{
    private readonly Mock<UserStateService> _mockUserStateService;
    private readonly LoginViewModel _loginViewModel;

    public LoginViewModelTests()
    {
        _mockUserStateService = new Mock<UserStateService>();
        _loginViewModel = new LoginViewModel(_mockUserStateService.Object);
    }

    [Fact]
    public async Task LoginAsync_WithValidCredentials_ShouldCallService()
    {
        // Arrange
        _loginViewModel.Username = "testuser";
        _loginViewModel.Password = "demo123";

        // Act
        await _loginViewModel.LoginCommand.ExecuteAsync(null);

        // Assert
        _mockUserStateService.Verify(x => x.LoginUser("testuser"), Times.Once);
    }

    [Theory]
    [InlineData("", "demo123")]
    [InlineData("user", "")]
    [InlineData("", "")]
    public async Task LoginAsync_WithInvalidCredentials_ShouldNotCallService(
        string username, string password)
    {
        // Arrange
        _loginViewModel.Username = username;
        _loginViewModel.Password = password;

        // Act
        await _loginViewModel.LoginCommand.ExecuteAsync(null);

        // Assert
        _mockUserStateService.Verify(x => x.LoginUser(It.IsAny<string>()), Times.Never);
    }
}

Testing Property Change Notifications

[Fact]
public void Username_WhenChanged_ShouldRaisePropertyChanged()
{
    // Arrange
    var propertyChangedRaised = false;
    _loginViewModel.PropertyChanged += (_, e) =>
    {
        if (e.PropertyName == nameof(LoginViewModel.Username))
            propertyChangedRaised = true;
    };

    // Act
    _loginViewModel.Username = "newuser";

    // Assert
    propertyChangedRaised.Should().BeTrue();
}

Running Tests on EndeavourOS

The beauty of xUnit on Linux is how seamless it is:

# Run all tests
dotnet test

# Run with verbose output
dotnet test --logger "console;verbosity=detailed"

# Run specific test class
dotnet test --filter "UserStateServiceTests"

# Run tests with coverage (install coverlet.collector first)
dotnet test --collect:"XPlat Code Coverage"

# Watch mode for continuous testing during development
dotnet watch test

Why Not NUnit or MSTest?

NUnit is still solid but feels dated:

  • More verbose syntax
  • Less intuitive parameterized tests
  • Requires more setup for async testing

MSTest is Microsoft’s framework but:

  • Less popular in the community
  • Fewer features compared to xUnit
  • Less flexible extensibility

Advanced xUnit Features You’ll Love

Custom Test Collections

[Collection("Database collection")]
public class UserServiceIntegrationTests
{
    // Tests that need to run sequentially
}

Conditional Tests

[FactOnLinux]
public void LinuxSpecificFeature_ShouldWork()
{
    // Only runs on Linux - perfect for EndeavourOS!
}

public class FactOnLinuxAttribute : FactAttribute
{
    public FactOnLinuxAttribute()
    {
        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            Skip = "Only runs on Linux";
        }
    }
}

My Recommendation

Go with xUnit. It’s the modern choice that will serve you well as your Avalonia applications grow in complexity. The learning curve is gentle, the documentation is excellent.

Start with the basic setup I showed above, and gradually add more sophisticated testing patterns as you become comfortable with the framework. Your future self will thank you for the comprehensive test coverage!

Mohammed Chami
Mohammed Chami
Articles: 45

Leave a Reply

Your email address will not be published. Required fields are marked *