TestFramework.Core
0.5.0
dotnet add package TestFramework.Core --version 0.5.0
NuGet\Install-Package TestFramework.Core -Version 0.5.0
<PackageReference Include="TestFramework.Core" Version="0.5.0" />
<PackageVersion Include="TestFramework.Core" Version="0.5.0" />
<PackageReference Include="TestFramework.Core" />
paket add TestFramework.Core --version 0.5.0
#r "nuget: TestFramework.Core, 0.5.0"
#:package TestFramework.Core@0.5.0
#addin nuget:?package=TestFramework.Core&version=0.5.0
#tool nuget:?package=TestFramework.Core&version=0.5.0
TestFramework.Core
TestFramework.Core is the timeline engine of the TestFramework ecosystem.
It provides the public API to:
- define integration-test workflows
- execute them with runtime inputs
- assert outcomes from an immutable run result
Install
dotnet add package TestFramework.Core
Quick Start
using TestFramework.Core.Timelines;
using TestFramework.Core.Timelines.Assertions;
using TestFramework.Core.Variables;
using Xunit;
public class CoreSample
{
private const string InputValue = "Alex";
private static readonly Timeline _timeline = Timeline.Create()
.SetVariable("name", Var.Const(InputValue))
.Transform("greeting", Var.Ref<string>("name"), name => $"Hello {name}")
.AssertVariable(Var.Ref<string>("greeting"), greeting => greeting == $"Hello {InputValue}")
.Build();
[Fact]
public async Task RunTimeline()
{
TimelineRun run = await _timeline.SetupRun().RunAsync();
run.EnsureRanToCompletion();
using (var assertionScope = run.AssertionScope())
{
run.Variable<string>("greeting").Should().Exist().And().Be($"Hello {InputValue}");
}
}
}
Common Building Blocks
Timeline.Create()to start the builderSetVariable,Transform,AssertVariablefor variable-driven data flowTrigger(...)andWaitForEvent(...)for actions and external synchronizationWithTimeOut(...),WithRetry(...)for reliability on unstable systems
Consumer-First Contract
For most users, the Core contract is intentionally small:
- Start with
Timeline.Create(). - Compose fluent steps and modifiers.
- Freeze the plan with
Build(). - Create a run with
SetupRun(...). - Execute with
RunAsync()and assert throughTimelineRun.
The scope split matters:
Build()usually belongs at class scope because it produces the reusable timeline definition.SetupRun(...)usually belongs at method scope because each call creates a per-run builder with run-specific services, variables, artifacts, or output wiring.
The package exposes additional public types for artifacts, environment integration, debugging, and the fluent builder composition model, but those are advanced surfaces. If you are writing tests rather than framework extensions, prefer the timeline builder, Var, TimelineRun, and the assertion handles as your main API.
Fluent API Discovery
The fluent API is one logical surface even though it is composed internally from several public interfaces.
For normal usage, treat these as the only concepts that matter:
Timeline.Create()starts composition- fluent builder verbs add steps and modifiers
Build()freezes the reusable definitionSetupRun(...)creates a per-run builderRunAsync()executes the run
The lower-level action interfaces remain public for compatibility and extension reasons, but they are intentionally de-emphasized in IntelliSense. If you discover those types directly, prefer returning to ITimelineBuilder, ITimelineBuilderModifier, and the fluent usage examples rather than learning the API through the interface lattice.
Extension-Facing Surface
You only need the lower-level public abstractions when you are extending the framework itself, for example by adding:
- custom triggers or events
- artifact describers and references
- environment-provider integrations
- runtime or debugging integrations
Those advanced surfaces are supported by the architecture docs, but they are secondary to the consumer workflow above.
Timeline Debugging
The recommended debugging path depends on what you need to see:
- Name important steps so failures and assertions point to stable labels.
- Pass
ITestOutputHelperintoSetupRun(...)when you want the timeline log in the test output stream. - Inspect the completed
TimelineRunfor stage state, step results, variables, and artifacts.
For most users, that post-run inspection path is the supported debugging workflow.
The lower-level debugger integration seam (IRunDebugger and related state types) remains available for custom tooling, but it is an advanced integration surface rather than the primary learning path.
Run Evidence (Widgets)
Post-run inspection tells you what a run did. run.Widgets is where it says what things looked like:
run.Widgets.Publish(new Widget
{
Kind = WidgetKinds.Screenshot,
Name = "after-login",
Form = DebugPreviewForm.Image,
Bytes = png,
Summary = "The dashboard, logged in"
});
The file lands in the run's own output and is attributed to the step and the attempt that produced it, so
retries keep every attempt rather than only the last. Publish hands back where it wrote the file, or
null if it could not, and never throws - failing to gather evidence must not add a second failure. WidgetKinds names the three the family produces -
Screenshot, Document, LogStream - and the kind is a plain string, so a package can introduce one
without waiting on a Core release.
For a run that is held at a breakpoint, register an IWidgetCaptureSource: the engine asks it for a
current picture while the run is stopped, which the last captured one no longer is.
This is the supported place for evidence. A package that writes screenshots into a folder of its own making has an arrangement only it and one tool understand.
Typical Pattern
- Build timeline once (usually static in test classes or other reusable class scope).
- Create a run with
SetupRun(...)inside the test or method that is about to execute it. - Add runtime variables/artifacts if needed.
- Run with
RunAsync(). - Assert with
EnsureRanToCompletion()and variable/artifact checks.
Persistent Environments
Most timelines should keep environment creation per run.
When a suite repeatedly needs the same expensive environment slice, Core also exposes PersistentEnvironmentContext<TSetup> as the lower-level reuse primitive.
Use it when all of the following are true:
- the environment shape is stable across many runs
- some components are expensive enough that recreating them dominates runtime
- those components can safely opt into
EnvComponentReuseMode.PersistentContext
The model is:
TSetup.CreateEnvironment()describes the full environment instance that future runs should receive.TSetup.GetPersistentComponentIdentifiers()selects the component roots that should be realized once and reused.PersistentEnvironmentContext<TSetup>.CreateEnvironment()produces fresh run environments with the persistent runtime state seeded back in.
Higher-level packages may wrap this primitive with package-specific helpers. In the container stack, DockerAzureHostedCollectionFixture<TState> is the xUnit-facing example of that pattern.
Target Frameworks
- .NET 8 (
net8.0) - .NET 10 (
net10.0)
| 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 was computed. 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
- Newtonsoft.Json (>= 13.0.4)
- xunit.abstractions (>= 2.0.3)
-
net8.0
- Newtonsoft.Json (>= 13.0.4)
- xunit.abstractions (>= 2.0.3)
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 |
|---|
New in 0.5.0: a run has somewhere to put the evidence it produced. RunContext.Widgets takes a screenshot, a log or a generated file, writes it into the run's own output, and attributes it to the step and the attempt that produced it - so a step that retried has one picture per attempt rather than only the last. A watching consumer can also ask a paused run for a fresh look at itself: a caller registers an IWidgetCaptureSource, the engine asks it while the run is held, and the answer waits until the widgets it counts have actually been delivered. A debugger receives them through ISupportsWidgets, which is a second interface rather than a wider IRunDebugger, so a debugger compiled against an earlier Core keeps working and simply does not receive them.
Also new in 0.5.0: ArtifactStore.CaptureVersion is public, so a package's own step can record a new version of an artifact with data the step already holds - captured at a boundary the run drove, with nothing for a reference to re-resolve. The write goes through the same funnel as Add, and being public adds two refusals: an instance this store does not hold, and version data of a foreign artifact kind. And a test marked with an attribute derived from a test attribute is identified as that test rather than as the host process, which used to cost such a suite its breakpoints, its baseline comparison and its re-run filter.
CHANGED in 0.5.0, and worth a look if you already record runs: the run journal now resolves under %USERPROFILE%\.testframework\Debug instead of %LOCALAPPDATA%\TestFramework\Debug. The existence of that folder is what switches recording on, so an existing installation records nothing until the tool creates the new one - reinstalling or starting the current launcher does it. Journals already written stay where they are; move them across if you want them, or point TESTFRAMEWORK_DEBUG_JOURNAL_DIR at the old path. The move is what keeps the folder usable as a handshake between two processes regardless of how either is packaged, which a path under AppData cannot promise.
Earlier: BREAKING (behaviour, introduced in 0.3.0 - whose release notes did not say so): teardown deletes every artifact a run declared, registered or discovered, unless the declaring step chains MarkReadonly(). Deleting is the default because a test that leaves its own data behind poisons the next run; MarkReadonly() is the single opt-out, it is stated where the artifact is declared, and nothing downstream can overrule it.
What changed for a consumer: discovery used to be safe by accident. Some finders returned references teardown could not delete - LocalIO's folder finder, the web package's SQL row finder, Azure's EF Core SQL finder - while Cosmos and Table discovery deleted. Now all of them delete unless told otherwise, so a timeline that discovers files or rows it does not own must add MarkReadonly() before upgrading.
New in this area: IMarkArtifactsReadonly, MarkReadonly() on the artifact-declaring builder steps, ArtifactInstance.IsReadonly, and ArtifactMarkedReadonlyException when something tries to deconstruct a readonly artifact. A reference separately reports whether it *can* be deconstructed; MarkReadonly() answers whether it *may* be, and teardown needs a yes from both.
Also in 0.3.0, and the only thing its own notes described: debugger protocol v4 - enums travel as names, a step's plan states its policies, and an assertion carries typed arguments rather than rendered sentences.