TestFramework.Container.Web 0.4.0

The owner has unlisted this package. This could mean that the package is deprecated, has security vulnerabilities or shouldn't be used anymore.
dotnet add package TestFramework.Container.Web --version 0.4.0
                    
NuGet\Install-Package TestFramework.Container.Web -Version 0.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TestFramework.Container.Web" Version="0.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TestFramework.Container.Web" Version="0.4.0" />
                    
Directory.Packages.props
<PackageReference Include="TestFramework.Container.Web" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TestFramework.Container.Web --version 0.4.0
                    
#r "nuget: TestFramework.Container.Web, 0.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TestFramework.Container.Web@0.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TestFramework.Container.Web&version=0.4.0
                    
Install as a Cake Addin
#tool nuget:?package=TestFramework.Container.Web&version=0.4.0
                    
Install as a Cake Tool

Icon

TestFramework.Container.Web

Serves the API, the database and the stubbed dependencies a TestFramework.Web timeline needs from Docker containers.

A timeline written against a deployed system runs here unchanged. It still names an identifier; this environment decides that the identifier is served by a container it starts, and publishes the address into the same configuration store a settings file would have filled.

Install

dotnet add package TestFramework.Container.Web

Targets net8.0 and net10.0. Needs a reachable Docker daemon.

Quickstart

Declare what the database is made of:

internal sealed class SampleSqlDefinition : DockerSqlDefinition
{
    public override SqlIdentifier Identifier => "main";

    protected override void Configure(DockerSqlBuilder builder) => builder
        .WithDatabase("SampleDb")
        .WithSchemaFromModels<Order, Customer>()
        .WithResetMode(SqlResetMode.RecreateDatabase);
}

Then point a run at it:

ConfigInstance config = ConfigInstance.Create()
    .LoadWebConfig()
    .AddWebSqlModels(models => models.For<Order>().Table("Orders").Key(x => x.Id).MaxLength(x => x.Name, 200))
    .Build();

Timeline timeline = Timeline.Create()
    .SetupArtifact("order")
    .Trigger(WebExt.Sql.Scalar<int>("main", "SELECT COUNT(1) FROM [Orders]")).Name("count")
    .Build();

TimelineRun run = await timeline.SetupRun(config)
    .SetEnv(DockerWebEnvironment.For<SampleSqlDefinition>())
    .AddArtifact("order",
        WebExt.Artifact.Sql.Row<Order>("main", Var.Const("1")),
        new SqlRowArtifactData<Order>(new Order { Id = 1, Name = "sample", Quantity = 3 }))
    .RunAsync();

run.EnsureRanToCompletion();
run.SqlScalar<int>("count").Should().Be(1);

LoadWebConfig() is required even with no Sql section in the settings: it registers the store the container publishes into.

Where The Schema Comes From

WithSchemaFromModels<...>() derives the tables from the run's own model registry, so the mappings a test registers with AddWebSqlModels shape the generated tables. What a CLR type cannot say — lengths, precision, identities, nullability — is declared alongside the mapping.

Generation covers schemas, tables, columns, nullability, identities and primary keys. Everything else a database needs belongs in a script, applied after the generated tables in the order the scripts are added:

builder.WithSchemaFromModels<Order>()
       .WithSchemaScript(SqlScript.FromFile("Schema/views.sql"))
       .WithSchemaScript(SqlScript.FromFile("Schema/reference-data.sql"));

A generated schema is scaffolding for a database the test owns, not a migration tool. Where the real schema is owned elsewhere — by migrations, or by whoever runs the server — use a script that mirrors it, because a table generated from test-side models proves only that the models agree with themselves.

Reset Modes

A container can outlive a single run, so what a previous run left behind is a real concern.

Mode Behaviour
None create the database when missing, keep whatever it contains
RunResetScript run WithResetScript(...) once the schema is in place
RecreateDatabase drop and create, so every run starts from the declared schema alone

Provisioning order is fixed: the database exists, then the generated tables, then the declared scripts, then the reset script. A reset that ran first would fail on the first run, when there is nothing yet to clear.

Running The Application Too

Declare the application the same way, pointing a configuration value at the database:

internal sealed class OrdersApiDefinition : DockerApiDefinition
{
    public override ApiIdentifier Identifier => "orders";

    // Named, not discovered. A relative path resolves against this source file.
    public override ContainerSource Source =>
        ContainerSource.Project("../Orders.Api/Orders.Api.csproj").WithTargetFramework("net10.0");

    protected override void Configure(DockerApiBuilder builder) => builder
        .WithEnvironmentName("Testing")
        .WithHealthPath("/health")
        .UseSql<SalesSqlDefinition>("ConnectionStrings:Sales")
        .WithSetting("Features:UseFakeClock", "true");
}

The test project does not reference the application project. That is the point: the application stays a black box, addressed only by the paths it exposes.

Both go into the same environment, and the timeline calls the API while asserting on the database behind it:

Timeline timeline = Timeline.Create()
    .Trigger(WebExt.Api.Http("orders").Post("api/orders")
        .WithJsonBody(Var.Const(new CreateOrder("container-order", 7))).Call()).Name("create")
    .FindArtifact("created", WebExt.ArtifactFinder.Sql.Where<Order>("sales", "Name = @name")
        .WithParameter("name", Var.Const("container-order")))
    .Build();

TimelineRun run = await timeline.SetupRun(config)
    .SetEnv(DockerWebEnvironment.For<SalesSqlDefinition>().Include<OrdersApiDefinition>())
    .RunAsync();

run.EnsureRanToCompletion();
run.ApiStatus("create").Should().Be(HttpStatusCode.Created);
run.SqlRow<Order>("created").Select(order => order.Quantity).Should().Be(7);

The response says what the API claims; the row says what actually happened.

How the application gets there

By default the SDK builds an image from the project — no Dockerfile, and the base image follows the project's framework, so net10.0 becomes mcr.microsoft.com/dotnet/aspnet:10.0. Two other strategies are one call away:

ContainerSource.Project(path)                     // SDK builds the image        ~8 s
ContainerSource.Project(path).BuiltOnHost()       // publish to temp, copy in    ~2 s
ContainerSource.Project(path).BuiltInContainer()  // built in Docker, clean host ~6 s

BuiltInContainer leaves no build output on the host at all and needs no feed credentials: the host restores with the configuration that already works, and the packages that produced are handed to the container as a pre-populated cache with every NuGet source cleared. It requires the target framework generation to match the SDK on the machine, and says so while planning rather than failing inside the container.

Other sources are available where a project is not the right answer:

ContainerSource.Image("orders-api:ci-1234")            // a pipeline already built it
ContainerSource.Directory(@"C:\out\orders-api")        // this exact folder

Whatever the source, the plan is written to the run log before anything starts and kept on the component state, so what actually ran is never something you have to infer.

How configuration gets there

Settings become a generated appsettings.<Environment>.json copied in beside the application, not a set of doubly underscored environment variables — a file can be read back when a run misbehaves. It is written to the run log, and kept on the state:

ApiComponentState state = run.EnvironmentContext.GetState<ApiComponentState>(DockerWebEnvironment.ApiComponentId);
RunningApi api = state.GetRequiredApi("orders");
// api.SettingsJson, api.BaseUrl, api.Plan.ProjectPath, api.Plan.Image, api.Plan.BuiltAtUtc

WithEnvironmentVariable(...) remains for the values that really are environment variables.

Applications are never reused

A database is worth keeping warm across runs; an application is not. A reused container would go on serving the code it started with, so an edit-and-rerun cycle would silently test the previous build. Databases have reset modes for stale data; there is no equivalent for a stale binary, so the application container is per-run and the SQL container is not.

Stubbing What The Application Calls

A stub definition comes from TestFramework.Web and says nothing about hosting — declaring it here is what decides that a container serves it:

internal sealed class PaymentsStubDefinition : StubDefinition
{
    public override StubIdentifier Identifier => "payments";

    protected override void Configure(StubMappingBuilder builder) => builder
        .OnGet("/api/rates/EUR")
            .RespondJson(HttpStatusCode.OK, new { currency = "EUR", rate = 1.08 })
        .OnPost("/api/charges")
            .WithHeader("Idempotency-Key")
            .RespondJson(HttpStatusCode.Created, new { id = "{{Random Type=Guid}}" }, useTemplating: true);
}

The application is pointed at it the same way it is pointed at a database:

protected override void Configure(DockerApiBuilder builder) => builder
    .UseSql<SalesSqlDefinition>("ConnectionStrings:Sales")
    .UseStub<PaymentsStubDefinition>("Services:Payments:BaseUrl");

DockerWebEnvironment.For<SalesSqlDefinition>()
    .Include<OrdersApiDefinition>()
    .IncludeStub<PaymentsStubDefinition>();

Then the timeline asserts on what the application sent outwards, which no response body can show:

.WaitForEvent(WebExt.Stub.Called("payments", HttpMethod.Post, "/api/charges")).Name("charged")
.Trigger(WebExt.Stub.Calls("payments")).Name("calls")

run.StubCall("charged").Select(call => call.Body).Should().Contain("\"amount\":30");
run.StubCalls("calls").Should().HaveCount(1);
run.StubUnmatchedCalls("calls").Should().HaveCount(0);   // nothing was called that was not declared

Mappings are declarative because a container cannot call back into the test process. Handlebars templating ({{request.body.amount}}) covers the cases a C# callback would otherwise be reached for.

Two things worth knowing:

  • Mappings are verified on startup. A mapping the server rejects is simply absent, and every call to it would answer 404 for no visible reason, so the component compares declared against loaded and fails immediately with the container log if they differ.
  • The image follows latest. Its publisher does not tag releases; pin it with UseStubImage(...) when a run has to be reproducible over time.

Serving The Frontend Too

A site is the application a browser loads: an Angular build, any other npm framework's output, or a plain folder of pages. Nothing framework-specific is baked into the container kind — a fixed nginx image gets the payload copied in, and the Source only says where that payload comes from:

internal sealed class ShopSiteDefinition : DockerSiteDefinition
{
    public override SiteIdentifier Identifier => "shop";   // the same string browser steps use

    public override SiteSource Source =>
        SiteSource.NpmProject("../../Shop.Frontend");       // npm run build, dist probed by convention

    protected override void Configure(DockerSiteBuilder builder) => builder
        .ProxyApi<OrdersApiDefinition>("/api");
}

DockerWebEnvironment.For<SalesSqlDefinition>()
    .Include<OrdersApiDefinition>()
    .Include<ShopSiteDefinition>();

The site publishes its host address into the Site configuration store, so a browser timeline resolves it by identifier like every other resource — and a deployed site is the same identifier with a Site:shop:BaseUrl entry instead of a container.

Where the payload comes from

Source Build environment needed
SiteSource.Directory(@"..\dist\shop\browser") none — an already-built folder
SiteSource.NpmProject("../Shop.Frontend") node + npm on the host; ng/vite come from node_modules
...NpmProject(...).BuiltInContainer() only Docker — sources are copied into a node image, npm ci and the build run there, the output ships
SiteSource.ContainerBuild("../docs", "hugomods/hugo", ["hugo"]).WithDistPath("public") the named build image; any toolchain
SiteSource.Image("shop-ui:ci-1234") none — runs as-is, with whatever server it bakes in

The plan is stated and logged before anything happens, exactly like an application's. An npm project without a declared WithDistPath(...) is probed by convention (dist/*/browser first, the Angular layout) — and exactly one answer is accepted; two plausible output folders are an error, not a guess.

How the browser reaches the API

The SPA either calls a relative path, or reads an absolute address from its runtime configuration. Both are declared on the definition, and they write different addresses on purpose:

App style Declaration Address written CORS
calls /api relative .ProxyApi<OrdersApiDefinition>("/api") network URL, inside the generated nginx config none needed — same origin, like production
reads apiBaseUrl from config .ConfigJsonApi<OrdersApiDefinition>("apiBaseUrl") host URL, in the generated assets/config.json the application must allow it

The proxied road works because the browser resolves a relative path against the page's own origin — the site container — whose nginx forwards it over the Docker network. The browser never needs a route to the API container at all.

A target may also be named by identifier instead of type: .ProxyApi("orders", "/api") resolves to the declared container when the environment runs one, and to the Api:orders:BaseUrl configuration entry when the backend is deployed elsewhere. Same store either way, so moving the API between the two is one Include<>().

Runtime configuration of any shape

The declarative bindings write into one JSON file (default assets/config.json), merging over the file the payload ships: a checked-in { "apiBaseUrl": null, "theme": "dark" } keeps theme. For every other shape — env.js, window.__env, a patched page — one composer covers it:

.WithConfigFile("env.js", ctx =>
    $"window.__env = {{ ordersApi: \"{ctx.Addresses.ApiBaseUrl("orders")}\" }};")

// or patch one field of the shipped JSON without rebuilding the rest:
.WithConfigFile("assets/config.json", ctx =>
    ctx.MergeJson(("apiBaseUrl", ctx.Addresses.ApiBaseUrl("orders").ToString())))

Every generated file — the nginx config included — is logged verbatim and kept on the component state, so a test can state what was actually served:

SiteComponentState state = run.EnvironmentContext.GetState<SiteComponentState>(DockerWebEnvironment.SiteComponentId);
RunningSite site = state.GetRequiredSite("shop");
// site.BaseUrl, site.NginxConfig, site.GeneratedFiles["assets/config.json"], site.Plan

What the server does for a SPA

Deep links fall back to index.html by default (WithoutSpaFallback() turns that off for a classic multi-page folder), location blocks carry generated comments naming the proxy_pass trailing-slash rule they encode, and WithNginxServerDirective(...) appends anything no declaration covers, verbatim. An Image source runs as-is: declaring proxy routes or fallback changes against it fails fast, because its server configuration is baked in.

Sites are per-run for the same reason applications are: a reused container would serve the previous build and proxy to containers that are gone. Linux containers are required, as they already are for SQL Server and the stub server.

Two Addresses, Again

The test process gets the host connection string; the application container gets the network one. Both describe the same database, and handing over the wrong one is the classic failure:

SqlServerComponentState state = run.EnvironmentContext.GetState<SqlServerComponentState>(DockerWebEnvironment.SqlServerComponentId);
SqlDatabaseEndpoint endpoint = state.GetRequiredDatabase("main");
// endpoint.HostConnectionString    -> localhost,<mapped port>   (published to the timeline)
// endpoint.NetworkConnectionString -> sqlserver,1433            (injected via UseSql)

UseSql<T>(...) always injects the network form, so this is one thing a test author cannot get wrong.

Tuning The Server

One container serves every declared database, so the server settings belong to the environment rather than to a definition:

DockerWebEnvironment.For<SampleSqlDefinition>()
    .Include<ReportingSqlDefinition>()
    .UseSqlImage("mcr.microsoft.com/mssql/server:2019-latest")
    .UseSqlMemoryLimit(2048)

Troubleshooting

Symptom Cause and fix
The run has no SQL configuration store LoadWebConfig() was not called on the config the run uses.
The run has no API configuration store The same, for an application.
declared N mapping(s) but the server loaded M The stub server rejected a mapping. Its log, already captured, names the file.
which no included definition declares A step, artifact or binding names an identifier no definition was included for. The message lists what is declared.
does not name a database The definition's Configure never called WithDatabase(...).
is not a plain identifier A database name goes into a statement verbatim, so only letters, digits and underscores are accepted.
the SQL Server container did not become usable The engine did not start within the readiness window. Check docker logs; a low memory limit is the usual cause.
did not answer within ... for an API The application failed to start. Its own log is already captured into the run output — read that before anything else.
both directly and from a resource binding A setting is written by WithSetting and by UseSql/UseStub at once, so which wins is not obvious. Remove one.
targets ..., so which one to run is ambiguous A multi-targeted project needs WithTargetFramework(...); picking silently would let a project change what a test runs.
No 'docker' executable was found The SDK shells out to the CLI to build an image. Install it, or use BuiltOnHost(), which needs none.
The Docker daemon is serving 'windows' containers The .NET base images used here are Linux images. Switch Docker Desktop, or name a matching image.
needs a .NET N SDK, and this machine restores with M An offline in-container build must target the SDK generation that resolved the packages. Target that framework, or use BuiltAsImage().
The container build for '...' failed The build output follows the message and the context is kept at the named path for inspection.
Rows survive between runs The default reset mode keeps them. Choose RecreateDatabase, or supply a reset script.
A code change seems to have no effect The application container is per-run and the framework builds the project itself, so this should not happen. Check the plan in the run log for which project was built.

Scope

This package runs the application under test, the SQL Server behind it, and the stubbed dependencies in front of it. It does not host an application in the test process; that is deliberately a different environment, not a mode of this one.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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

Adds DockerSiteDefinition: static web sites (Angular and other SPA builds, or any dist folder) served by an nginx container on the same environment network as the application, with declared proxy routes, SPA history fallback and generated runtime-config files. Sites publish their address into WebConfigStore<SiteConfig>, so browser timelines resolve them by identifier like every other resource.
BREAKING - RunningApi gained a NetworkBaseUrl parameter after BaseUrl, and application containers now carry the network alias 'api-<identifier>' so sites can proxy to them. Requires TestFramework.Web 0.4.0.