Startup
A project may declare one IPreceptStartup to register services and run assembly-wide setup:
public sealed class Startup : IPreceptStartup
{
public void ConfigureServices(IServiceCollection services, PreceptSettings settings)
{
services.AddPreceptApi();
services.AddPreceptWeb();
services.AddPreceptData(cs => new NpgsqlConnection(cs));
}
public Task BeforeRunAsync(IServiceProvider services) => SeedReferenceDataAsync();
}
ConfigureServices is required — write it with an empty body if the suite registers nothing, since an explicit declaration is what stops a mistyped signature from compiling. BeforeRunAsync and AfterRunAsync are optional.
BeforeRunAsync runs once, just before the first test runs, and AfterRunAsync once the last has finished — and neither runs when the platform only lists tests. dotnet run -- --list-tests, and a test explorer refreshing its tree after a build, open a session and discover the suite without running anything, and a hook that seeds a database or signs a browser in is not something a tree refresh should do. ConfigureServices does run on discovery, because the container it builds is what discovery reads settings from; keep it to registrations, and put work that touches an environment in BeforeRunAsync.
If BeforeRunAsync throws, no test runs — not even a class's own [BeforeSuite] — and every test the run had selected is reported as failed with the hook's exception, so the cause reaches the console, the TRX, a test explorer and every reporter rather than only a log. AfterRunAsync still runs, because a setup that failed halfway is the one most in need of its teardown.
Each AddPrecept… binds that module's own section of precept.json and takes a callback for
anything the file cannot say — a base address the startup only learns at run time, a secret read
from a vault rather than committed:
services.AddPreceptApi(api => api.BaseUrl = stub.BaseUrl);
services.AddPreceptWeb(web => web.Headless = false);
The callback runs when the settings are first resolved rather than here, so it may close over
something this method has only just created. The settings parameter carries the run's own settings
— parallelism, retries, the environment, the filter — and not a module's; read one of those with settings.GetSection<T>() if a decision here depends on it.
Anything registered here is resolvable from PreceptTestContext.Current.Services.
The run's container is disposed once AfterRunAsync has returned, so a singleton that implements
IDisposable or IAsyncDisposable — a container host, test data created for the whole run, a
client holding connections — gets its teardown without the project having to arrange it.