Rystem is a open-source framework to improve the System namespace in .Net
$ dotnet add package Rystem.Test.XUnitAdvanced XUnit integration testing framework with built-in Dependency Injection and ASP.NET Core Test Host support. Perfect for testing APIs, services, and complex application architectures.
dotnet add package Rystem.Test.XUnit
Rystem.Test.XUnit requires XUnit v3. After recent XUnit updates, ensure you have the correct package versions installed.
Required packages in your test project:
<ItemGroup>
<PackageReference Include="Rystem.Test.XUnit" Version="9.1.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit.v3" Version="3.0.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
Key differences from XUnit v2:
xunit.v3 instead of xunit packagexunit.runner.visualstudio version 3.1.4+Quick project setup:
# Create test project
dotnet new xunit -n MyApp.Tests
# Remove old xunit package (if present)
dotnet remove package xunit
# Add required packages
dotnet add package Rystem.Test.XUnit
dotnet add package xunit.v3 --version 3.0.1
dotnet add package xunit.runner.visualstudio --version 3.1.4
dotnet add package Microsoft.NET.Test.Sdk --version 17.14.1
Perfect for testing services, repositories, and business logic without API layer.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Rystem.Test.XUnit;
namespace MyApp.Tests
{
public class Startup : StartupHelper
{
protected override string? AppSettingsFileName => "appsettings.json";
protected override bool HasTestHost => false; // ❌ No HTTP server
protected override Type? TypeToChooseTheRightAssemblyToRetrieveSecretsForConfiguration => typeof(Startup);
protected override Type? TypeToChooseTheRightAssemblyWithControllersToMap => null; // Not needed
protected override IServiceCollection ConfigureClientServices(IServiceCollection services, IConfiguration configuration)
{
// Add your business services here
services.AddMyBusinessLogic();
services.AddMyRepositories();
services.AddMyTestHelpers();
return services;
}
}
}using Xunit;
namespace MyApp.Tests
{
public class BusinessLogicTest
{
private readonly IBookManager _bookManager;
private readonly ITestHelpers _testHelpers;
// Constructor injection works automatically! 🎉
public BusinessLogicTest(IBookManager bookManager, ITestHelpers testHelpers)
{
_bookManager = bookManager;
_testHelpers = testHelpers;
}
[Fact]
public async Task CreateBook_ShouldReturnValidBook()
{
// Arrange
var bookId = Guid.NewGuid();
// Act
var book = await _bookManager.CreateBookAsync(bookId);
// Assert
Assert.NotNull(book);
Assert.Equal(bookId, book.Id);
}
[Theory]
[InlineData("Title 1")]
[InlineData("Title 2")]
public async Task UpdateBookTitle_ShouldSucceed(string title)
{
// Arrange
var book = await _testHelpers.CreateTestBookAsync();
// Act
await _bookManager.UpdateTitleAsync(book.Id, title);
var updated = await _bookManager.GetByIdAsync(book.Id);
// Assert
Assert.Equal(title, updated.Title);
}
}
}Full integration testing with real HTTP requests to your ASP.NET Core API.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Rystem.Test.XUnit;
namespace MyApp.Api.Tests
{
public class Startup : StartupHelper
{
protected override string? AppSettingsFileName => "appsettings.test.json";
protected override bool HasTestHost => true; // ✅ Enable HTTP server
protected override bool WithHttps => true; // HTTPS enabled
protected override bool AddHealthCheck => true; // Add /healthz endpoint
// Use Startup or any controller class from your API project
protected override Type? TypeToChooseTheRightAssemblyWithControllersToMap => typeof(Program); // Your API's Program class
// Use test project's Startup to load User Secrets
protected override Type? TypeToChooseTheRightAssemblyToRetrieveSecretsForConfiguration => typeof(Startup);
// Configure services for TEST CLIENT (HTTP consumers)
protected override IServiceCollection ConfigureClientServices(IServiceCollection services, IConfiguration configuration)
{
// Add HTTP client to call your API
services.AddHttpClient<IMyApiClient, MyApiClient>(client =>
{
client.BaseAddress = new Uri("https://localhost");
});
return services;
}
// Configure services for TEST SERVER (API dependencies)
protected override async ValueTask ConfigureServerServicesAsync(IServiceCollection services, IConfiguration configuration)
{
// Add your API services
services.AddControllers();
services.AddEndpointsApiExplorer();
services.AddMyApiServices();
services.AddMyRepositories();
services.AddMyBusinessLogic();
return;
}
// Configure middleware pipeline for TEST SERVER
protected override async ValueTask ConfigureServerMiddlewareAsync(IApplicationBuilder app, IServiceProvider serviceProvider)
{
// Configure your API middleware chain
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
return;
}
}
}using Xunit;
namespace MyApp.Api.Tests
{
public class ApiIntegrationTest
{
private readonly IMyApiClient _apiClient;
public ApiIntegrationTest(IMyApiClient apiClient)
{
_apiClient = apiClient;
}
[Theory]
[InlineData("user@example.com")]
[InlineData("admin@example.com")]
public async Task Login_ShouldReturnUser(string email)
{
// Act - Real HTTP call to localhost test server
var user = await _apiClient.LoginAsync(email);
// Assert
Assert.NotNull(user);
Assert.Equal(email, user.Email);
}
[Fact]
public async Task GetBooks_ShouldReturnList()
{
// Arrange
await _apiClient.LoginAsync("user@example.com");
// Act
var books = await _apiClient.GetBooksAsync();
// Assert
Assert.NotEmpty(books);
}
[Fact]
public async Task CreateBook_ThenGetById_ShouldMatch()
{
// Arrange
var createRequest = new CreateBookRequest
{
Title = "Test Book",
Author = "Test Author"
};
// Act
var created = await _apiClient.CreateBookAsync(createRequest);
var retrieved = await _apiClient.GetBookByIdAsync(created.Id);
// Assert
Assert.Equal(created.Id, retrieved.Id);
Assert.Equal(createRequest.Title, retrieved.Title);
}
}
}AppSettingsFileName (Required)Path to your appsettings file for test configuration.
protected override string? AppSettingsFileName => "appsettings.test.json";Best Practice: Use separate appsettings.test.json for test configurations.
HasTestHost (Required)Enables or disables the ASP.NET Core Test Server.
protected override bool HasTestHost => true; // Enable test server
protected override bool HasTestHost => false; // Disable (business logic only)When to enable:
When to disable:
TypeToChooseTheRightAssemblyWithControllersToMap (Required if HasTestHost = true)Specifies which assembly contains your API controllers/endpoints.
// Option 1: Use Program class from your API project
protected override Type? TypeToChooseTheRightAssemblyWithControllersToMap => typeof(Program);
// Option 2: Use any controller from your API project
protected override Type? TypeToChooseTheRightAssemblyWithControllersToMap => typeof(BooksController);
// Option 3: Not needed if HasTestHost = false
protected override Type? TypeToChooseTheRightAssemblyWithControllersToMap => null;Purpose: Automatically discovers and maps all controllers/endpoints in that assembly.
TypeToChooseTheRightAssemblyToRetrieveSecretsForConfiguration (Required)Specifies which assembly to load User Secrets from (Visual Studio Secret Manager).
// Usually points to your TEST project's Startup class
protected override Type? TypeToChooseTheRightAssemblyToRetrieveSecretsForConfiguration => typeof(Startup);User Secrets Example:
{
"ConnectionStrings:Database": "Server=...;Database=Test;",
"ExternalApi:ApiKey": "secret-key-here"
}How to manage secrets:
# Right-click test project in Visual Studio → Manage User Secrets
# Or use CLI:
dotnet user-secrets set "ConnectionStrings:Database" "Server=localhost;..."WithHttps (Optional, default: true)Configures test server to use HTTPS.
protected override bool WithHttps => true; // https://localhost:443
protected override bool WithHttps => false; // http://localhostPreserveExecutionContext (Optional, default: false)Preserves execution context across async operations.
protected override bool PreserveExecutionContext => true;When to enable:
AsyncLocal<T>AddHealthCheck (Optional, default: true)Automatically adds /healthz endpoint to test server.
protected override bool AddHealthCheck => true; // Adds /healthz endpointValidation: Test framework automatically calls /healthz after server startup to ensure health.
ConfigureClientServices (Required)Configure dependency injection for test consumers (your test classes).
protected override IServiceCollection ConfigureClientServices(IServiceCollection services, IConfiguration configuration)
{
// Add services used by test classes
services.AddHttpClient<IMyApiClient, MyApiClient>(client =>
{
client.BaseAddress = new Uri("https://localhost");
});
services.AddSingleton<ITestDataGenerator, TestDataGenerator>();
return services;
}ConfigureServerServicesAsync (Virtual, implement if HasTestHost = true)Configure dependency injection for test server (API dependencies).
protected override async ValueTask ConfigureServerServicesAsync(IServiceCollection services, IConfiguration configuration)
{
// Add your API services here
services.AddControllers();
services.AddMyBusinessLogic();
services.AddMyRepositories();
// Configure database for testing
services.AddDbContext<MyDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));
return;
}ConfigureServerMiddlewareAsync (Virtual, implement if HasTestHost = true)Configure middleware pipeline for test server.
protected override async ValueTask ConfigureServerMiddlewareAsync(IApplicationBuilder app, IServiceProvider serviceProvider)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors("AllowAll");
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGrpcService<MyGrpcService>();
});
return;
}┌─────────────────────────────────────────────────────────────┐
│ XUnit Test Runner │
└───────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ StartupHelper (Abstract Base) │
│ - Loads appsettings.json │
│ - Loads User Secrets │
│ - Configures Host Builder │
└───────────────────────────┬─────────────────────────────────┘
│
┌─────────────┴─────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────┐
│ ConfigureClientServices│ │ ConfigureServerServicesAsync│
│ (Test DI Container) │ │ (API DI Container) │
│ - HTTP Clients │ │ - Controllers │
│ - Test Helpers │ │ - Business Logic │
│ - Mocks │ │ - Repositories │
└───────────┬─────────────┘ └──────────┬──────────────────┘
│ │
│ ┌───────────┴───────────────┐
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ ASP.NET Core Test Server │ │
│ │ - Middleware Pipeline │ │
│ │ - Endpoint Routing │ │
│ │ - Authentication │ │
│ └────────────┬───────────────┘ │
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────────────┐
│ Constructor Injection in Test Classes │
│ public MyTest(IMyService service, IHttpClientFactory http) │
└────────────────────────────────────────────────────────────┘
public class Startup : StartupHelper
{
protected override string? AppSettingsFileName => "appsettings.json";
protected override bool HasTestHost => false;
protected override Type? TypeToChooseTheRightAssemblyToRetrieveSecretsForConfiguration => typeof(Startup);
protected override Type? TypeToChooseTheRightAssemblyWithControllersToMap => null;
protected override IServiceCollection ConfigureClientServices(IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<TestDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));
services.AddRepositories();
return services;
}
}
public class RepositoryTest
{
private readonly IRepository<Book> _bookRepository;
public RepositoryTest(IRepository<Book> bookRepository)
{
_bookRepository = bookRepository;
}
[Fact]
public async Task InsertAndRetrieve_ShouldWork()
{
var book = new Book { Id = Guid.NewGuid(), Title = "Test" };
await _bookRepository.InsertAsync(book);
var retrieved = await _bookRepository.GetAsync(book.Id);
Assert.Equal(book.Title, retrieved.Title);
}
}public class Startup : StartupHelper
{
protected override string? AppSettingsFileName => "appsettings.test.json";
protected override bool HasTestHost => true;
protected override Type? TypeToChooseTheRightAssemblyWithControllersToMap => typeof(Program);
protected override Type? TypeToChooseTheRightAssemblyToRetrieveSecretsForConfiguration => typeof(Startup);
protected override IServiceCollection ConfigureClientServices(IServiceCollection services, IConfiguration configuration)
{
services.AddHttpClient<IBridgeInspectionApi, BridgeInspectionApi>(client =>
{
client.BaseAddress = new Uri("https://localhost");
});
return services;
}
protected override async ValueTask ConfigureServerServicesAsync(IServiceCollection services, IConfiguration configuration)
{
services.AddControllers();
services.AddAuthentication("TestScheme")
.AddScheme<TestAuthOptions, TestAuthHandler>("TestScheme", options => { });
services.AddAuthorization();
services.AddMyApiServices();
return;
}
protected override async ValueTask ConfigureServerMiddlewareAsync(IApplicationBuilder app, IServiceProvider serviceProvider)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());
return;
}
}
public class InspectionTest
{
private readonly IBridgeInspectionApi _api;
public InspectionTest(IBridgeInspectionApi api)
{
_api = api;
}
[Theory]
[InlineData("user@example.com")]
public async Task GetBridgesAndInspections_ShouldReturnData(string email)
{
// Login
var user = await _api.LoginAsAsync(email);
Assert.NotNull(user);
// Get bridges
var bridges = await _api.BridgeApi.ListAsync();
Assert.NotEmpty(bridges);
// Get inspections for first bridge
var bridge = bridges.First();
var inspections = await _api.InspectionApi.ListAsync(bridge.Id);
Assert.NotEmpty(inspections);
}
[Fact]
public async Task DownloadInspectionZip_ShouldReturnBytes()
{
await _api.LoginAsAsync("admin@example.com");
var bridges = await _api.BridgeApi.ListAsync();
var inspections = await _api.InspectionApi.ListAsync(bridges.First().Id);
var zipBytes = await _api.InspectionApi.DownloadInspectionsFile(
bridges.First().Id,
inspections.First().Id
);
Assert.NotEmpty(zipBytes);
}
}services.AddHttpClient<IMyClient, MyClient>((serviceProvider, client) =>
{
client.BaseAddress = new Uri("https://localhost");
client.DefaultRequestHeaders.Add("X-Test-Environment", "true");
client.DefaultRequestHeaders.Add("X-Api-Key", "test-key");
});protected override IServiceCollection ConfigureClientServices(IServiceCollection services, IConfiguration configuration)
{
// Replace real external API with mock
services.AddSingleton<IExternalApiClient, MockExternalApiClient>();
return services;
}protected override async ValueTask ConfigureServerServicesAsync(IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<MyDbContext>(options =>
options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}")); // Unique per test run
return;
}