Add xUnit v3 support#108
Merged
Merged
Conversation
Add DeviceRunners.VisualRunners.Xunit3 project with in-process test discovery and execution using xUnit v3's ExtensibilityPointFactory API. New projects: - src/DeviceRunners.VisualRunners.Xunit3 - Visual runner for xUnit v3 - test/TestProject.Xunit3Tests - xUnit v3 test data project - sample/test/DeviceTestingKitApp.MauiLibrary.Xunit3Tests - Sample tests Key architecture decisions: - Uses xunit.v3.extensibility.core directly for in-process discovery/execution - Test class libraries reference xunit.v3.extensibility.core + xunit.v3.assert (NOT xunit.v3.core which forces OutputType=Exe and injects Main) - PrivateAssets=compile prevents v3 types from conflicting with v2 types - Follows guidance from xunit/xunit#3112 discussion Changes: - Directory.Packages.props: Add xunit.v3.* package versions (3.2.2) - DeviceRunners.slnx: Add new projects - Sample MauiProgram.cs: Add .AddXunit3() alongside existing runners - docs/articles/xunit-v3-support.md: Usage guide - Test base classes: Add virtual TestAssembly property for framework flexibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Key fixes: - Await the ValueTask from ITestFrameworkDiscoverer.Find() which runs discovery asynchronously on a ThreadPool thread - Call TestContext.SetForInitialization() before ExtensibilityPointFactory as required by the xUnit v3 framework - Use ExtensibilityPointFactory.GetTestFramework() directly instead of ConsoleRunnerInProcess for simpler in-process discovery/execution - Make test base classes support overridable ExpectedTestCount All 148 tests pass (including 12 new xUnit v3 tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Critical fixes: - Await RunTestCases ValueTask (was discarded — exceptions swallowed, potential deadlock if execution fails before ITestAssemblyFinished) - Dispose ITestFramework via await using (implements IAsyncDisposable, was leaking on every discovery/execution call) - Wire Xunit3DiagnosticMessageSink to TestContext.SetForInitialization so xUnit framework errors are forwarded to IDiagnosticsManager Thread safety: - Add lock to Xunit3ExecutionMessageSink.OnMessage() to protect Dictionary mutations from concurrent test execution Error handling: - Handle IErrorMessage, ITestAssemblyCleanupFailure, ITestCollectionCleanupFailure, ITestClassCleanupFailure, ITestCaseCleanupFailure, ITestCleanupFailure in execution sink - Flush remaining pending results on ITestAssemblyFinished for tests that emitted ITestStarting but never ITestFinished - Improve Xunit3DiagnosticMessageSink to also handle IInternalDiagnosticMessage Cleanup: - Delete dead Xunit3DiscoveryMessageSink (not needed with callback-based ITestFrameworkDiscoverer.Find()) - Update docs to match actual implementation (ExtensibilityPointFactory API, not ConsoleRunnerInProcess; note config files not yet supported) - Add dotnet-reportgenerator-globaltool for code coverage - Add artifacts/coverage/ to .gitignore - Fix output capture test to not pass vacuously Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update UITestCase and UITheoryTestCase to use the correct xUnit v3 API: - Use base constructor instead of non-existent Initialize() method - Pass resolved values from TestIntrospectionHelper.GetTestCaseDetails directly to base class constructors - Await CreateTests() which returns ValueTask<IReadOnlyCollection> Update UIFactDiscoverer and UITheoryDiscoverer to call TestIntrospectionHelper.GetTestCaseDetails with the required ITestFrameworkDiscoveryOptions parameter and pass decomposed details to test case constructors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New project: - DeviceRunners.UITesting.Xunit3 — [UIFact] and [UITheory] attributes for xUnit v3 that dispatch test execution to the UI thread via ISelfExecutingXunitTestCase Config loading: - Xunit3TestDiscoverer now loads xunit.runner.json via ConfigReader_Json.LoadFromJson() using FileSystemUtils stream - TestAssemblyConfiguration is passed through to both discovery and execution options - Xunit3TestAssemblyInfo now holds TestAssemblyConfiguration Updates: - DeviceRunners.slnx — added UITesting.Xunit3 - Sample DeviceTests.csproj — added UITesting.Xunit3 reference - Docs — updated limitations, added UI testing section Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The sample DeviceTests project references both xunit v2 and v3 test libraries. Fix type conflicts by: - Remove UITesting.Xunit3 reference from device tests (users choose one or the other based on their xunit version) - Add PrivateAssets=compile to sample Xunit3Tests project's v3 packages to prevent v3 types from flowing transitively into the device tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enable PreEnumerateTheories in both discoverer and runner so each theory data row becomes its own Xunit3TestCaseInfo (matching xUnit v2). Without this, all theory rows share one test case ID and the last row's result overwrites earlier ones — silently hiding failures (e.g. a theory where row 1 fails but row 3 passes reports Passed). With pre-enumeration: - DataTest(1), DataTest(2), DataTest(3) each get a unique test case - ExpectedTestCount goes back to 8 (matching v2 parity) - Each row's pass/fail is tracked independently Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UIFactDiscoverer now derives from FactDiscoverer (inherits parameter validation, generic method checks, and trait handling). UITheoryDiscoverer now derives from TheoryDiscoverer (inherits theory data enumeration, serialization checks, no-data error handling, skip semantics, and trait propagation). This matches the v2 pattern where UIFactDiscoverer : FactDiscoverer and UITheoryDiscoverer : TheoryDiscoverer. Added TraitsHelper to replicate the internal ToReadWrite() extension for converting read-only trait dictionaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts: # .gitignore # DeviceRunners.slnx # test/DeviceRunners.VisualRunners.Tests/Testing/TestDiscovererTests.cs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bUnit uses AngleSharp which has TFM compatibility issues (net8.0 vs net10.0). The bUnit tests (CounterComponentClickTests, LoginComponentTests) use a fake Razor renderer, not real component rendering. Remove them and the bUnit package reference. The remaining tests use HtmlRenderer or pure unit testing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use logical assembly name (assembly.GetName().Name + ".dll") instead of FileSystemUtils.GetAssemblyFileName() which returns empty string on WASM (Assembly.Location is empty in WebAssembly). This matches the approach used by the v2 XunitReflectionTestDiscoverer. The xunit v3 API (ExtensibilityPointFactory.GetTestFramework) is already fully reflection-based and works on WASM without special handling. The only issue was our wrapper code using file-path-based assembly matching. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add DeviceTestingKitApp.BlazorLibrary.Xunit3Tests with unit tests for CounterViewModel, CounterValueFormatter, and BlazorSemanticAnnouncer using xunit v3 (mirrors the existing v2 XunitTests). Wire up AddXunit3() in BrowserTests Program.cs alongside AddXunit(). Unlike v2 which needs useReflection: true for WASM, v3 uses ExtensibilityPointFactory which is already reflection-based. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
xunit v3's TestAssemblyRunner.OnTestAssemblyStarting calls Path.GetFileNameWithoutExtension(TestAssembly.AssemblyPath) which crashes on WASM because Assembly.Location returns empty string. Add WasmXunit3TestAssembly that subclasses XunitTestAssembly and re-implements IXunitTestAssembly to return a logical assembly path (assembly.GetName().Name + ".dll") when Assembly.Location is empty. Add WasmXunit3TestFramework that creates WASM-safe assembly objects for both discovery and execution. Update Xunit3TestDiscoverer and Xunit3TestRunner to use the WASM-safe framework automatically when Assembly.Location is empty. WASM BrowserTests results: 58 passed, 5 intentional failures, 3 skipped (previously 24 non-intentional failures from AssemblyName guard). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create DeviceTestingKitApp.BlazorLibrary.NUnitTests project with sample NUnit tests (UnitTests, CounterViewModelTests, CounterValueFormatterTests) - Wire up NUnit in BrowserTests via AddNUnit() alongside existing xunit - Add VisualRunners.NUnit project reference to BrowserTests - Add new project to DeviceRunners.slnx NUnit's DefaultTestAssemblyBuilder already uses reflection-based discovery (takes Assembly object directly), so no special WASM-specific discoverer is needed — unlike xunit v2 which required XunitReflectionTestDiscoverer. The NUnitTestAssemblyInfoBuilder also short-circuits the runner's Load to avoid filesystem access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root causes of NUnit producing 0 tests on WASM: 1. Thread creation: NUnit's SimpleWorkItemDispatcher calls new Thread().Start() which throws PlatformNotSupportedException on single-threaded WASM. Fix: Set RunOnMainThread=true to use MainThreadWorkItemDispatcher which executes inline without spawning threads. 2. EventPump thread: NUnit creates an EventPump thread for async event delivery. Fix: Set SynchronousEvents=true to bypass the EventPump. 3. Assembly path: FileSystemUtils.GetAssemblyFileName used Assembly.Location which returns empty string on WASM. Fix: Fall back to AssemblyName + .dll. 4. Silent failures: Added diagnostic messages when NUnit marks assemblies as not runnable or discovers 0 tests, making debugging much easier. 5. WorkDirectory: NUnit calls Directory.GetCurrentDirectory() internally. Fix: Provide WorkDirectory=. in configuration to short-circuit this. The RunOnMainThread and SynchronousEvents settings are safe for all platforms since the NUnit runner is already called from a background thread via AsyncUtils.RunAsync on MAUI — 'main thread' just means 'calling thread'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This reverts commit 88f0b03.
This reverts commit f65cf50.
Add comprehensive documentation for how xUnit v3 works on WASM/Blazor: - New 'WASM / Blazor Browser Support' section in xunit-v3-support.md explaining the automatic WASM detection, WasmXunit3TestAssembly, WasmXunit3TestFramework, and the interface dispatch remapping pattern - Desktop vs WASM comparison table (assembly location, threading, result output, configuration, [TestFramework] attribute support) - Comparison with xUnit v2 WASM approach (reflection-based vs targeted assembly path workaround) - Updated technical-architecture-overview.md with xunit v3 framework entries, WASM architecture details, and UITesting.Xunit3 - Updated visual-runner-in-the-ide.md Blazor example to show .AddXunit3() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ndroid/iOS too The empty Assembly.Location problem is not WASM-specific. On Android, assemblies are loaded from APK streams; on iOS, from AOT bundles. Both return empty Assembly.Location just like WASM. Renames: - WasmXunit3TestAssembly → InMemoryXunit3TestAssembly - WasmXunit3TestFramework → InMemoryXunit3TestFramework Updated docs to explain the cross-platform Assembly.Location behavior with a platform comparison table showing which platforms have empty locations and why. Also documents how xunit v2 handled this differently per platform (dummy files on Android, reflection discoverer on WASM) vs xunit v3's unified InMemory approach. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Find callback now returns false when cancellation is requested, allowing faster abort during discovery of large assemblies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 46 new tests covering the xUnit v3 message sink, runner, and discoverer that would have caught bugs found during development: Xunit3ExecutionMessageSinkTests (31 tests): - Theory aggregation: worst-status-wins (Failed > Passed > Skipped > NotRun) - Duration and output accumulation across theory rows - Multiple failure error message/stack trace concatenation - ITestCaseFinished flush behavior - Orphan result flush on ITestAssemblyFinished - All cleanup failure types routed to diagnostics - Output + error preserved together on failing tests - Skip reason first-wins semantics - Cancellation token stops message pump - Unknown test case IDs handled gracefully - Thread-safe concurrent message delivery - ResultReported event firing and overwrite behavior - Result channel integration and null safety Xunit3RunnerAdvancedTests (8 tests): - Re-run same tests produces fresh results - ResultReported event fires for each test case - Selective execution only runs specified tests - Empty test case list doesn't throw - Non-Xunit3 test cases are filtered out - Cancellation stops execution cleanly - Failed test with output captures both Xunit3DiscovererAdvancedTests (7 tests): - Test cases have correct metadata - Null results before execution - Cancellation during discovery - Expected test count matches - Rediscovery returns consistent results - Assembly filename populated - Configuration has device defaults Infrastructure changes: - Add InternalsVisibleTo for test project on Xunit3 library - Add xunit.v3.common and xunit.v3.runner.common to test project - Suppress xUnit1051 analyzer (v3 rule, project uses v2 runner) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…own project - Remove ParallelizeTestCollections and MaxParallelThreads overrides from all discoverers (v2 and v3) — parallelization is the test author's choice - Remove PrivateAssets="compile" from Xunit3 library — types flow naturally - Create DeviceRunners.VisualRunners.Xunit3.Tests project using xunit v3 as the test framework (no v2/v3 conflict, proper TestContext.Current usage) - Move all xunit3 test files to the new project - Add CancellationToken parameter to HomeViewModel.StartAssemblyScanAsync - Clean up v2 test project (remove xunit.v3.* packages and NoWarn hack) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix UniqueID collision in InMemoryXunit3TestAssembly by passing explicit uniqueID derived from logicalAssemblyPath to base XunitTestAssembly ctor (previously all in-memory assemblies shared the same UniqueID) - Fix UITestCase/UITheoryTestCase to run CreateTests() through the ExceptionAggregator, properly handling data enumeration failures as test failures/skips instead of unhandled exceptions - Fix runner test silently passing via early return — now asserts NotNull - Fix cancellation test to assert discovery returns fewer tests - Fix 'does not throw' tests to verify no results were recorded and cancellation leaves tests unrun - Fix LoadContent/WaitForLayout in sample UI tests to fail on timeout instead of hanging indefinitely or returning silently Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ibrary The runner library (DeviceRunners.VisualRunners.Xunit3) is designed to be co-hosted with the v2 runner in apps that run both v2 and v3 tests. Without PrivateAssets="compile", the xunit v3 attribute types (FactAttribute, TheoryAttribute, etc.) flow transitively and conflict with xunit v2 types in the same namespace, causing CS0433 errors. Consuming test libraries that need xunit v3 types reference xunit.v3.extensibility.core directly and are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d error parsing The test app's Console.WriteLine diagnostic messages (e.g. 'Framework error: System.Runtime.InteropServices.COMException') match MSBuild's canonical error format when flowing through the Exec task. This causes MSBuild to fail with exit code 1 even though all tests pass (results arrive via TCP). Redirect stdout/stderr and consume them asynchronously to prevent contamination of MSBuild's output parsing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t MSBuild error parsing" This reverts commit 5e18319.
…capture xunit v3's executor was being called on the UI thread, causing it to capture WinUI's DispatcherQueueSynchronizationContext. When xunit's internal async infrastructure then posts callbacks from thread pool threads through the captured WinUI context, it triggers COMExceptions (RPC_E_WRONG_THREAD). Run the executor on a thread pool thread where SynchronizationContext.Current is null. UIFact/UITheory tests still dispatch to the UI thread correctly via UIThreadCoordinator.DispatchAsync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Wrap xunit v3 discovery in Task.Run to avoid SyncContext capture - Marshal DiagnosticsViewModel.Messages.Add to UI thread via SynchronizationContext - Handle OperationCanceledException in RunTestsSafe without logging as error Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…reading tests - TestCaseViewModel.OnTestResultReported now marshals to UI thread via SynchronizationContext (same pattern as DiagnosticsViewModel fix) - Prevents COMException on WinUI when xunit v3 reports results from thread pool threads after the Task.Run isolation - Add comprehensive threading test suite verifying: - Discovery/execution don't deadlock with blocking SyncContext - Execution moves to thread pool (not caller thread) - Concurrent executions complete without deadlock (AsyncLock) - DiagnosticsViewModel marshals via captured SyncContext - DiagnosticsViewModel falls back to direct add without SyncContext Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove unused 'firstResults' variable in Xunit3RunnerAdvancedTests - Remove unused 'using NSubstitute' in Xunit3DiscovererAdvancedTests - OCE handling in RunTestsSafe was already addressed in prior commit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 88 out of 90 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
test/DeviceRunners.VisualRunners.Xunit3.Tests/Testing/Xunit3RunnerAdvancedTests.cs:88
reportedResultsis aList<T>that is appended to fromResultReportedcallbacks. Those callbacks may be raised from multiple threads (xUnit v3 message sink is explicitly thread-safe and can report from parallel execution), so this list mutation can race and make the test flaky. Use a thread-safe collection (or protectAddwith a lock).
src/DeviceRunners.VisualRunners/ViewModels/HomeViewModel.cs:83StartAssemblyScanAsyncnow accepts aCancellationToken, but it’s only passed into discovery. If_options.AutoStartis enabled, the subsequent run (RunEverythingAsync/_runner.RunTestsAsync) ignores the token, so cancellation can still lead to a full test run starting. Consider propagating the token through the auto-run path (and early-exiting if cancellation is already requested).
…variable - Use ConcurrentBag<T> for ResultReported callbacks (may fire from parallel xunit v3 execution threads) - Remove unused firstResult variable in RerunSingleTest test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Verify the UIThreadCoordinator plumbing works correctly: - Throws InvalidOperationException when no coordinator is configured - Delegates to configured coordinator - Coordinator receives and executes dispatched work - UIFactAttribute/UITheoryAttribute have correct discoverer attributes - UITestCase/UITheoryTestCase implement ISelfExecutingXunitTestCase Full end-to-end UIFact/UITheory execution is tested by MAUI device sample apps in CI (DynamicUITests, TestPageUITests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…upport # Conflicts: # sample/test/DeviceTestingKitApp.BrowserTests/DeviceTestingKitApp.BrowserTests.csproj # sample/test/DeviceTestingKitApp.BrowserTests/Program.cs
…etry OperationCanceledException is now caught separately so that IsLoaded is not set on cancellation, allowing subsequent discovery attempts. IsLoaded is only set after successful discovery or on non-cancellation errors (where retry would likely fail again anyway). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…reading contracts - Xunit3TestDiscoverer.DiscoverAsync now re-throws OperationCanceledException instead of swallowing it, so HomeViewModel's cancellation-aware catch block correctly prevents IsLoaded from being set (enabling retry). - IDiagnosticsManager.DiagnosticMessageReceived now documents that handlers may be invoked on any thread and must not block. - InMemoryXunit3TestFramework.CreateForAssembly documents that custom [TestFramework] attributes are not consulted when Assembly.Location is empty (matching v2 behavior on these platforms). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
xunit v3's MessageBus spawns a dedicated Thread for message delivery, which throws PlatformNotSupportedException on single-threaded platforms (WASM). Setting SynchronousMessageReporting=true tells the framework to use a synchronous message bus instead, matching the approach already used by the v2 runner (XunitReflectionTestRunner). Applied unconditionally since threaded message reporting adds no value in a device runner context where results are processed by the visual runner's own pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
RunTestsAsync_ExecutesOnThreadPoolThread asserted that result callback thread IDs differ from the caller's thread ID. This is unreliable because async/await releases the caller's thread back to the pool, allowing Task.Run to reuse it — causing the assertion to fail non-deterministically on macOS-Intel and Windows CI runners. Replaced with RunTestsAsync_ReportsResultsForAllTestCases which verifies the meaningful behavior (every test case gets a result callback with correct status) rather than an implementation detail (thread scheduling). The SyncContext non-capture guarantee is already tested by the dedicated RunTestsAsync_DoesNotCaptureCallerSyncContext and RunTestsAsync_WithCustomSyncContext_DoesNotDeadlock tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On threaded platforms, the async MessageBus pump avoids contention on the message sink lock by draining messages from a dedicated thread. Setting SynchronousMessageReporting=true unconditionally forced every parallel test worker to serialize on that lock inline, degrading throughput. Now only applied when OperatingSystem.IsBrowser() is true (where Thread cannot be spawned). Desktop/mobile retain full parallel execution perf. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds xUnit v3 as a supported test framework in DeviceRunners, alongside xUnit v2 and NUnit. Tests written with xUnit v3 can be discovered, executed, and reported through both the visual runner and CLI runner on all platforms — Android, iOS, macOS, Windows, and WASM.
Usage
Test library setup
xUnit v3 test assemblies must be class libraries (not executables) because the MAUI/Blazor app is the host process. Reference the lower-level xUnit v3 packages:
Registration
.AddXunit3()works the same on every platform — no flags, nouseReflection, no per-platform configuration. It can be used alongside.AddXunit()and.AddNUnit().UI testing
[UIFact]and[UITheory]attributes are available via theDeviceRunners.UITesting.Xunit3package:How it works
Discovery and execution
The runner uses xUnit v3's in-process extensibility APIs directly (
ITestFrameworkDiscoverer.Find()/ITestFrameworkExecutor.RunTestCases()). No separate test process is launched — everything runs inside the app.Threading model
Task.Runto escape the caller'sSynchronizationContext(e.g. WinUI dispatcher), preventing deadlocks while preserving full parallelism.MessageBuspump on threaded platforms (desktop, Android, iOS) for maximum throughput. On WASM (whereThreadcannot be spawned), synchronous reporting is enabled viaOperatingSystem.IsBrowser().SynchronizationContext.Post.OperatingSystem.IsBrowser()) rather than compile-time or based onAssembly.Locationemptiness.Platform compatibility
On Android, iOS, and WASM,
Assembly.Locationreturns an empty string because assemblies are loaded from streams rather than files. xUnit v3'sXunitTestAssemblyuses this path internally and crashes when it's empty.DeviceRunners handles this transparently with
InMemoryXunit3TestAssembly— a subclass that provides a logical path derived from the assembly name. The same code path works on all platforms; the runner auto-detects whether in-memory handling is needed at runtime.UI thread dispatch
[UIFact]/[UITheory]dispatch the entire test lifecycle (class construction,IAsyncLifetime, test method invocation, and disposal) to the UI thread.What's in this PR
New packages
DeviceRunners.VisualRunners.Xunit3DeviceRunners.UITesting.Xunit3[UIFact]/[UITheory]attributes and test case runnersSamples
DeviceTestingKitApp.MauiLibrary.Xunit3Tests— MAUI library tests (CounterView, VisualElement, Converter, SemanticAnnouncer, UIFact/UITheory)DeviceTestingKitApp.BlazorLibrary.Xunit3Tests— Blazor library tests (CounterViewModel, Formatter, SemanticAnnouncer)Tests
TestProject.Xunit3Tests— fixture assembly (facts, theories, skips, output)DeviceRunners.VisualRunners.Xunit3.Tests— dedicated v3 unit test project with discoverer, runner, home view model, and threading testsTestDiscovererTests.Xunit3,TestRunnerTests.Xunit3,HomeViewModelTests.Xunit3)Docs
docs/articles/xunit-v3-support.md— quick start, package architecture, platform compatibility table, in-memory assembly handling, desktop vs device/WASM differencesOther
Directory.Packages.propsandDeviceRunners.slnxWaitForPageLayout) for stable UI test navigationKnown limitations
[TestFramework]— On platforms whereAssembly.Locationis empty (Android, iOS, WASM), the[TestFramework]assembly attribute is bypassed. Custom xUnit test frameworks only work on desktop. Tracked upstream: xunit/xunit#3096.dotnet teston device TFMs — xUnit v3 device test projects cannot usedotnet testor VS Test Explorer for device TFMs. This is a fundamental xUnit v3 architecture constraint for device testing. However,dotnet testworks on the host TFM (net10.0) for quick local iteration.PrivateAssets="compile"— TheDeviceRunners.VisualRunners.Xunit3package usesPrivateAssets="compile"on its xunit.v3 dependencies to avoid type conflicts when co-hosted with the v2 runner. Consumers must bring their own xunit.v3 package references (which they always do since they're writing xunit tests).