Testing¶
Microsoft.UI.Reactor (Reactor)'s render loop is deterministic and synchronous. A Component
mounted in a unit test renders, runs effects, accepts state updates,
re-renders, and disposes through the same code path the WinUI host
uses — minus the WinUI tree at the bottom. That makes the unit layer
fast (the framework's own xUnit run finishes in seconds), and it
keeps test bodies focused on the component's behavior instead of the
windowing chrome.
Reactor has three test suites, one per project, plus the doc pipeline which compiles every published sample:
class Counter : Component
{
public override Element Render()
{
var (count, setCount) = UseState(0);
return VStack(8,
TextBlock($"Count: {count}").FontSize(20).Bold(),
Button("Increment", () => setCount(count + 1))
).Padding(16);
}
}

Reference¶
| Suite | Project | Runner | When to reach for it |
|---|---|---|---|
| Unit | tests/Reactor.Tests/ |
xUnit | Hook semantics, reducer logic, modifier chains, reconciliation algorithms, Yoga layout, analyzer rules. Fastest loop, no WinUI window. |
| Self-test | tests/Reactor.SelfTests/ (fixtures in Reactor.AppTests.Host) |
MSTest wrapping a TAP subprocess | Component renders into a real WinUI tree; assertions via VisualTreeHelper. |
| App E2E | tests/Reactor.AppTests/ |
MSTest + winapp ui | Real user input, UIA properties as seen by assistive tech, cross-process behavior. |
Every suite runs on Microsoft.Testing.Platform. Full-suite commands are plain:
dotnet test tests/Reactor.Tests -p:Platform=x64
dotnet test tests/Reactor.SelfTests
dotnet test tests/Reactor.AppTests
For targeted runs, use the runner's current filter syntax: xUnit suites
prefer MTP's --filter-class / --filter-method family, while the MSTest
self-test and E2E suites keep VSTest-style --filter expressions:
dotnet test tests/Reactor.Tests --filter-class "*ReconcilerMountUpdateTests*"
dotnet test tests/Reactor.SelfTests --filter "ClassName~SkipReportingTests"
dotnet test tests/Reactor.AppTests --filter "ClassName=Microsoft.UI.Reactor.AppTests.Tests.AccessibilityTests"
The doc apps under docs/_pipeline/apps/ are a fourth compile gate
rather than a test suite: mur docs compile builds every one of them,
so a snippet in this guide that names a removed API fails the docs
build.
Caveat: A headless unit test cannot construct any
Microsoft.UI.Xamlobject. There is no XAML application object behind an xUnit run, sonew Button(), a brush, a geometry, aBitmapImage, or anyAutomationPeer-derived type throwsCOMExceptionthe moment you touch it. Unit tests may exercise pure managed logic plus WinRT value structs and enums —Elementrecords, modifiers, hooks, layout math, the accessibility scanner — and nothing that materializes a control. The moment a test needs a live control, it belongs in a self-test fixture, not intests/Reactor.Tests.
Unit-level fixtures¶
This section is for tests inside this repository. Reactor's component lifecycle (
BeginRender→Render→FlushEffects→RunCleanups),Component.ContextandContextScopeare internal. They are visible totests/Reactor.Testsonly becausesrc/Reactor/Reactor.csprojgrants itInternalsVisibleTo, so the helper below will not compile in a consumer test project — it is shown to explain how Reactor's own fixtures are written, not as code to copy into your app's tests. To test your own components from outside the repo, use the public surface shown in Structural assertions, or drive a real control from a self-test.
The lifecycle is used directly by ContextSystemSelfHostTests and
ComponentModelIntegrationTests in tests/Reactor.Tests/, wrapped in a
per-class helper:
private static Element MountComponent(
Component component, ContextScope scope,
Dictionary<ContextBase, object?>? contextValues = null)
{
if (contextValues is { Count: > 0 })
scope.Push(contextValues);
try
{
component.Context.BeginRender(() => { }, scope);
var element = component.Render();
component.Context.FlushEffects();
return element;
}
finally
{
if (contextValues is { Count: > 0 })
scope.Pop(contextValues.Count);
}
}
The helper returns the root element. The test then drives state via the
component's own public surface (a property the component exposes, or a
captured setter from UseState), calls the helper again to re-render,
and asserts. When the fixture owns effects with dispose lambdas, call
component.Context.RunCleanups() before the next test starts. Pushing
and popping the ContextScope in a try/finally matters: a test that
throws mid-render would otherwise leak its context values into the next
test in the class.
The full pattern is in ComponentModelIntegrationTests.cs — that file
mounts a component with state + context + effects, drives 5 distinct
lifecycle transitions, and asserts the effect log after each. Use it
as the template when adding a unit fixture for a new hook.
Effect-aware async tests¶
UseEffect does not fire during render. It fires when the component's
context flushes effects — which the unit Mount helper above does
inline. Tests that exercise effect ordering must observe the log
between mount and the next render, not during render:
// Effect-aware component used as a fixture target. UseEffect fires on the
// next flush, not during render — tests must wait for the flush before
// observing the side effect's log entry (see testing.md, "Async patterns").
class EffectfulCounter : Component
{
public List<string> Log { get; } = new();
public override Element Render()
{
var (count, setCount) = UseState(0);
UseEffect(() =>
{
Log.Add($"effect:{count}");
return () => Log.Add($"cleanup:{count}");
}, count);
return Button($"count={count}", () => setCount(count + 1))
.AutomationName($"Counter is {count}");
}
}
A test for EffectfulCounter mounts the component, asserts
Log = ["effect:0"], increments state, re-renders, and asserts
Log = ["effect:0", "cleanup:0", "effect:5"]. The cleanup from the
previous effect runs before the new effect's body — that's the
contract tests/Reactor.Tests/ComponentModelIntegrationTests.cs codifies.
For genuinely async work (an HTTP fetch, a timer), don't hand-roll it
inside UseEffect: UseResource already owns
the CancellationToken, the loading/error states, and cancellation on
unmount, so a test can drive it by controlling the fetcher it was
handed. When the component must own its own task, expose the
completion Task so the test can await it deterministically — or
inject a fake clock via UseContext of a clock interface and tick it
forward by hand. Avoid Thread.Sleep in tests; it leaks wall-clock
time into the suite and makes CI flaky.
Structural assertions on the element tree¶
There is no golden-file snapshot harness in Reactor, and none is
needed: Element is a record, so a rendered tree is a value you can
assert against directly. Pattern-match the shape you care about
instead of stringifying the whole tree — a structural assertion names
the property under test, so its failure message points at the defect
rather than at a 200-line text diff:
[Fact]
public void Component_TProps_Renders_With_Props()
{
var comp = new GreetingComponent { Props = "Alice" };
var el = comp.Render();
Assert.IsType<TextBlockElement>(el);
Assert.Equal("Hello, Alice!", ((TextBlockElement)el).Content);
}
Assert on the slots the component is responsible for, not on every field of every element. A test that pins padding, font size, and child order together fails for three unrelated reasons and tells you which one only after you read the diff. Never assert on anything carrying a timestamp, a generated id, or a hash — normalize it or factor it out of the component under test.
Accessibility scanner integration¶
AccessibilityScanner.Scan(root) walks an element
tree and returns a List<A11yDiagnostic>, one per finding, each
carrying a rule Id ("A11Y_001" … ), a Severity, a
WcagCriterion, and a Fix suggestion. It takes an Element, not a
control, so it runs in the headless unit suite:
// AccessibilityScanner fixture targets. The scanner walks an element tree
// and returns one A11yDiagnostic per finding, each carrying a rule Id
// ("A11Y_001" = icon-only Button with no accessible name).
class IconOnlyButton : Component
{
public override Element Render() =>
Button(TextBlock("🔍")); // icon content, no accessible name
}
class NamedButton : Component
{
public override Element Render() =>
Button(TextBlock("🔍"), null).AutomationName("Search");
}
[Fact]
public void A11Y_001_IconButton_Without_AutomationName()
{
var tree = VStack(
Button(TextBlock("🔍"), null) // icon content, no AutomationName
);
var findings = AccessibilityScanner.Scan(tree);
Assert.Contains(findings, f => f.Id == "A11Y_001");
}
[Fact]
public void A11Y_001_IconButton_With_AutomationName_Passes()
{
var tree = VStack(
Button(TextBlock("🔍"), null).AutomationName("Search")
);
var findings = AccessibilityScanner.Scan(tree);
Assert.DoesNotContain(findings, f => f.Id == "A11Y_001");
}
Assert DoesNotContain on the specific rule id rather than Empty on
the whole list: a fixture that asserts the scan is completely clean
starts failing the day an unrelated rule is added, and the failure
will not be about the thing the test was written to protect.
The same scanner backs the in-app dev menu's "Run accessibility scan" button, so a fixture that passes here is the same shape that passes in the running app. Treat scanner-clean as the standing bar for every new component you ship.
Self-tests (real WinUI tree)¶
Reactor.SelfTests is the layer between the unit suite (pure C#, no
WinUI) and the full E2E suite (winapp ui). A self-test mounts a
real fixture into the Reactor.AppTests.Host window, walks the WinUI
visual tree, and emits TAP. The MSTest wrapper in SelfTestBatch.cs
parses the TAP and surfaces one test method per fixture.
To add a self-test:
- Add a new fixture file under
tests/Reactor.AppTests.Host/SelfTest/Fixtures/returning the component under test wrapped in a small assertion harness. - Register it in two places in
tests/Reactor.AppTests.Host/SelfTest/SelfTestFixtureRegistry.cs— theAllFixtureslist and theCreate()switch. Miss the second and--list-fixturesreports a name the run cannot produce, which shows up as a confusing "missing fixture" failure rather than a compile error. - The MSTest wrapper picks it up at discovery time via
--list-fixtures; no code change needed on the test runner side.
E2E fixtures have the same two-place split, in
tests/Reactor.AppTests.Host/FixtureRegistry.cs (AllFixtures plus the
Build switch). --list-fixtures is self-test-only, so nothing warns
you about the E2E half.
Reach for a self-test when the unit layer can't see the answer — e.g. when the WinUI control's measured size affects the component's behavior, or when an automation peer's role depends on the materialized XAML control class.
A fixture asserts with H.Check(name, condition). When the machine
genuinely cannot run a check — a locked desktop, an OS version without
the API — use H.Skip(name, reason) instead of quietly returning. A
fixture that skipped every check and asserted nothing is reported as
Skipped, not Passed, so "this machine could not test it" stays
distinguishable from "this machine tested it and it worked". Prefer
asserting the environment probe itself and skipping only the part that
depends on it, so the fixture still proves something everywhere. Put an
issue number in the reason when the skip marks a real product gap: a
skip is never evidence the product works, only that this run did not
establish otherwise.
Tips¶
Don't drive the unit fixture from Task.Delay. If an effect
schedules async work, expose its completion task so the test can
await it. Wall-clock delays leak into the suite and make CI flaky.
Assert on the tree's structure, not on rendered pixels. Element
records give you typed slots you can pattern-match; an actual rendered
bitmap depends on font rendering, DPI, and platform Composition — none
of which belong in a unit test.
Run the accessibility scan in every fixture's teardown. It's
cheap, it takes an Element rather than a control so it works
headlessly, and it puts the scanner's output next to the test that
introduced the problem.
Use Reactor.AppTests only where xUnit and self-tests can't reach.
The winapp ui E2E suite is the slow lane; reserve it for keyboard navigation,
focus order, and click sequences that depend on Composition or input
routing.
Next Steps¶
- Hooks — Previous in the learning path: the primitives a fixture exercises.
- Effects —
UseEffectlifecycle and cleanup, including the flush ordering tested above. - Accessibility — The scanner's rules and how to extend it with project-specific checks.
- Dev Tooling —
murCLI, preview mode, and the doc-pipeline harness that powers the screenshots on this page. - Components — Render purity rules that make the unit layer worth the investment.