SpawnDev.UnitTesting.Browser
2.5.7
dotnet add package SpawnDev.UnitTesting.Browser --version 2.5.7
NuGet\Install-Package SpawnDev.UnitTesting.Browser -Version 2.5.7
<PackageReference Include="SpawnDev.UnitTesting.Browser" Version="2.5.7" />
<PackageVersion Include="SpawnDev.UnitTesting.Browser" Version="2.5.7" />
<PackageReference Include="SpawnDev.UnitTesting.Browser" />
paket add SpawnDev.UnitTesting.Browser --version 2.5.7
#r "nuget: SpawnDev.UnitTesting.Browser, 2.5.7"
#:package SpawnDev.UnitTesting.Browser@2.5.7
#addin nuget:?package=SpawnDev.UnitTesting.Browser&version=2.5.7
#tool nuget:?package=SpawnDev.UnitTesting.Browser&version=2.5.7
NuGet
SpawnDev.UnitTesting
A lightweight, cross-platform unit testing framework for .NET. Works everywhere - Blazor WASM, WPF, Console, and more.
Supersedes SpawnDev.Blazor.UnitTesting with a platform-agnostic core.
Features
- Cross-platform - no Blazor, WPF, or browser dependencies in the core library
- Simple API -
[TestMethod]attribute andUnitTestRunnerfor reflection-based discovery - Async support - full
async Tasktest methods with configurable timeouts - Dependency injection - test classes resolved via
IServiceProviderandActivatorUtilities - Unsupported tests - throw
UnsupportedTestExceptionto skip tests gracefully - Blazor UI -
SpawnDev.UnitTesting.BlazorprovidesUnitTestsViewRazor component - WPF Desktop UI -
SpawnDev.UnitTesting.DesktopprovidesUnitTestsControlandUnitTestsWindow - Console runner -
ConsoleRunnerfor subprocess-based test execution (used by PlaywrightMultiTest) - Lazy test resolution - single-test mode avoids loading all method metadata, preventing .NET 10 JIT crashes with heavy generic types
Quick Start
1. Create test classes
using SpawnDev.UnitTesting;
public class MyTests
{
[TestMethod]
public async Task AdditionTest()
{
int result = 1 + 1;
if (result != 2) throw new Exception($"Expected 2, got {result}");
}
[TestMethod]
public async Task UnsupportedFeatureTest()
{
throw new UnsupportedTestException("Feature not available on this platform");
}
}
2. Run tests programmatically
var runner = new UnitTestRunner();
runner.SetTestTypes(new[] { typeof(MyTests) });
runner.TestStatusChanged += () => Console.WriteLine($"Progress: {runner.Tests.Count(t => t.State == TestState.Done)}/{runner.Tests.Count}");
await runner.RunTests();
3. Dependency Injection
Test constructors are injected via ActivatorUtilities.CreateInstance. Register services and test classes in the service container:
var services = new ServiceCollection();
services.AddSingleton<IMyService, MyService>();
services.AddSingleton<MyTests>();
var sp = services.BuildServiceProvider();
var runner = new UnitTestRunner(sp, true); // true = auto-discover all tests
await runner.RunTests();
Resolution order for test class instances:
- Custom resolver via
OnUnitTestResolverEvent IServiceProvider.GetService(type)- for registered servicesActivatorUtilities.CreateInstance(sp, type)- for DI-injected constructorsActivator.CreateInstance(type)- parameterless constructor fallback
One instance per test class, reused across all [TestMethod] calls. Implement IDisposable for cleanup.
4. Console Runner
ConsoleRunner enables subprocess-based test execution. Each test runs in its own process, isolating failures and enabling parallel test harnesses like PlaywrightMultiTest.
// Program.cs
using SpawnDev.UnitTesting;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<MyTests>();
var sp = services.BuildServiceProvider();
// Skip full discovery when running a single test for better performance
// and to avoid .NET 10 JIT issues with heavy generic types
var runner = new UnitTestRunner(sp, false);
if (args.Length == 0)
{
// List mode: discover all tests
runner.SetTestTypes(new[] { typeof(MyTests) });
}
else
{
// Single test mode: register types without method reflection
runner.RegisterTestTypes(new[] { typeof(MyTests) });
}
await ConsoleRunner.Run(args, runner);
Usage:
myapp.exe- lists all test names (one per line)myapp.exe MyTests.AdditionTest- runs that test, outputsTEST: {json}to stdout
How it works:
- No args: calls
SetTestTypesfor full method discovery, printsClassName.MethodNamefor each test - With test name arg: uses
ResolveSingleTestto find and run only the requested method, then serializes theUnitTestresult as JSON
5. Lazy Test Resolution (v2.5.1+)
When running a single test via ConsoleRunner, ResolveSingleTest resolves the method by name using Type.GetMethod() instead of loading all method metadata via GetMethods(). This is critical for test classes that reference heavy generic types (e.g., ILGPU kernels, ArrayView<T>) in non-test methods.
// Register types without method discovery
runner.RegisterTestTypes(new[] { typeof(MyTests) });
// Resolve and run a single test by name
var test = runner.ResolveSingleTest("MyTests.AdditionTest");
if (test != null)
{
await runner.RunTest(test);
}
Why this matters: On .NET 10, GetMethods() forces the JIT to compile type metadata for all method signatures. When test classes contain methods referencing complex generic types, this can race with COM interop threads and cause intermittent Internal CLR error (0x80131506) crashes. Lazy resolution avoids this by only loading the specific method being tested.
6. Test Timeouts
Default timeout is 30 seconds per test. Override per-test or globally:
// Per-test timeout
[TestMethod(Timeout = 120000)] // 2 minutes
public async Task SlowTest() { /* ... */ }
// Global default
runner.DefaultTimeoutMs = 60000; // 1 minute
runner.DefaultTimeoutMs = 0; // disable timeout
7. Retry on Failure (v2.5.2+)
For tests whose failure mode is genuine external-infrastructure flake (network hops, signaling servers, STUN/TURN, DNS), set RetryCount on the attribute. The test re-runs up to N times on Error; Success and Unsupported short-circuit the loop.
[TestMethod(Timeout = 180000, RetryCount = 3)]
public async Task SignalingPathTest() { /* ... */ }
Per-attempt state (Error, ResultText, StackTrace) is reset between tries so a passing retry doesn't carry over the prior attempt's error. UnitTest.AttemptsConsumed is populated so you can see "passed on attempt 2 of 4" in the results. Duration is cumulative across all attempts.
Don't use this to mask library races. If a test fails because of a genuine concurrency bug, fix the bug. RetryCount is for infrastructure hiccups outside your code's control.
8. Test Categories (v2.5.2+)
Tag tests with a Category string for grouping or filtering. Common uses: opt-in stress tests, smoke suites, integration-only subsets.
[TestMethod(Category = "Stress")]
public async Task HundredMBTensorTransfer() { /* ... */ }
[TestMethod(Category = "Smoke")]
public async Task CriticalPathTest() { /* ... */ }
The category flows through to UnitTest.Category for downstream filtering; the runner itself does not pre-filter by category - consuming harnesses decide which categories to include/exclude at run time.
9. Test Result Text
Tests can return a string for additional result information:
[TestMethod]
public async Task<string> InfoTest()
{
int count = ProcessItems();
return $"Processed {count} items";
}
10. Blazor UI (SpawnDev.UnitTesting.Blazor)
@using SpawnDev.UnitTesting.Blazor
<UnitTestsView TestAssemblies="_assemblies"></UnitTestsView>
@code {
IEnumerable<Assembly>? _assemblies = new List<Assembly> {
typeof(MyTests).Assembly
};
}
Supports live latest.json result output via ResultsDirectory (OPFS FileSystemDirectoryHandle in browser).
11. WPF Desktop UI (SpawnDev.UnitTesting.Desktop)
Option A - Standalone Window:
var testWindow = new SpawnDev.UnitTesting.Desktop.UnitTestsWindow
{
TestTypes = new[] { typeof(MyTests) },
AutoRun = true,
ResultsDirectory = "path/to/results",
CloseOnComplete = true,
};
testWindow.Show();
Option B - Embeddable UserControl:
<local:UnitTestsControl x:Name="TestControl" />
TestControl.TestTypes = new[] { typeof(MyTests) };
TestControl.AutoRun = true;
API Reference
UnitTestRunner
| Method | Description |
|---|---|
SetTestTypes(IEnumerable<Type>) |
Registers test types and discovers all [TestMethod] methods via reflection |
SetTestAssemblies(IEnumerable<Assembly>) |
Scans assemblies for types with [TestMethod] methods |
RegisterTestTypes(IEnumerable<Type>) |
Registers types without method discovery (for use with ResolveSingleTest) |
ResolveSingleTest(string) |
Resolves one test by ClassName.MethodName without loading all method metadata |
FindAllTests() |
Discovers tests in all loaded assemblies |
RunTest(UnitTest) |
Runs a single test |
RunTests() |
Runs all discovered tests |
RunTestsByClass(string) |
Runs all tests in a class |
RunTestByName(string, string) |
Runs a test by class and method name |
ResetTests() |
Resets all tests to initial state and disposes cached instances |
CancelTests() |
Cancels the current test run |
Test Result Types
| Outcome | How to signal |
|---|---|
| Pass | Return normally (no exception) |
| Fail | Throw any Exception |
| Skip | Throw UnsupportedTestException("reason") |
| Timeout | Exceeds TestMethodAttribute.Timeout or DefaultTimeoutMs |
UnitTest Properties
| Property | Type | Description |
|---|---|---|
TestName |
string |
ClassName.MethodName |
TestTypeName |
string |
Short class name |
TestMethodName |
string |
Method name |
Result |
TestResult |
None, Error, Success, Unsupported |
State |
TestState |
None, Running, Done |
Duration |
double |
Execution time in milliseconds |
Error |
string |
Error message if failed |
StackTrace |
string |
Stack trace if failed |
ResultText |
string |
Success message or skip reason |
AttemptsConsumed |
int |
Retries used before final result (0 = passed on first attempt) |
Category |
string |
Category tag from TestMethodAttribute.Category (empty when unset) |
Changelog
v2.5.2
- Added
TestMethodAttribute.RetryCount- retry the test up to N times onTestResult.Error.SuccessandUnsupportedshort-circuit the loop. Per-attempt state (Error,ResultText,StackTrace) is reset between tries; cumulativeDurationacross all attempts. - Added
TestMethodAttribute.Category- string tag for grouping/filtering tests (e.g."Stress","Smoke"). Propagated toUnitTest.Categoryfor downstream filtering by consuming harnesses. - Added
UnitTest.AttemptsConsumed- populated byUnitTestRunner.RunTestso results can report "passed on attempt 2 of 4". - Added
UnitTest.Category- mirrors the attribute value for easy access without re-reflecting.
v2.5.1
- Fixed .NET 10 JIT crash - eager method reflection via
GetMethods()forced the CLR to compile type metadata for all method signatures at process startup, causing intermittentInternal CLR error (0x80131506)when test classes referenced heavy generic types (ILGPU kernels, etc.) - Added
RegisterTestTypes()- registers types without method discovery - Added
ResolveSingleTest()- resolves a single test by name without loading all methods - Added
UnitTestJsonContext- source-generated JSON serialization for enum safety - Improved
SetTestAssemblies()withReflectionTypeLoadExceptionhandling ConsoleRunneruses lazy resolution as fallback for single-test subprocess mode
v2.5.0
- Added
SpawnDev.UnitTesting.Desktop- WPF test runner library UnitTestsWindowandUnitTestsControlfor WPF applications
v2.4.2
- Added
JsonNumberHandling.AllowNamedFloatingPointLiteralsfor float serialization safety - Live
latest.jsonresult output for real-time test monitoring
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Components.WebAssembly (>= 10.0.0)
- SpawnDev.SpawnJS (>= 0.0.1)
- SpawnDev.UnitTesting (>= 2.5.6)
-
net8.0
- Microsoft.AspNetCore.Components.WebAssembly (>= 8.0.19)
- SpawnDev.SpawnJS (>= 0.0.1)
- SpawnDev.UnitTesting (>= 2.5.6)
-
net9.0
- Microsoft.AspNetCore.Components.WebAssembly (>= 9.0.8)
- SpawnDev.SpawnJS (>= 0.0.1)
- SpawnDev.UnitTesting (>= 2.5.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.5.7 | 86 | 7/23/2026 |