Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: Bug report
about: Something in Alba isn't behaving as documented
title: ''
labels: bug
assignees: ''
---

**Describe the bug**
A clear description of what's going wrong.

**To reproduce**
A minimal failing test or code sample:

```csharp
// your scenario here
```

**Expected behavior**
What you expected to happen.

**Environment**
- Alba version:
- .NET version:
- Bootstrapping style (IHostBuilder / WebApplicationBuilder / WebApplicationFactory):
20 changes: 20 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an improvement or new capability for Alba
title: ''
labels: enhancement
assignees: ''
---

**What problem are you trying to solve?**
Describe the testing scenario Alba doesn't cover well today.

**Proposed solution**
What you'd like the API to look like, if you have something in mind:

```csharp
// sketch of the ideal usage
```

**Alternatives considered**
Any workarounds you're using now.
48 changes: 48 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Contributing to Alba

Thanks for your interest in contributing!

## Building and testing

Alba builds with the .NET SDK pinned in `global.json` and uses a [Nuke](https://nuke.build) build:

```bash
# Compile everything (Release)
./build.cmd # or ./build.sh on Linux/macOS

# Run the full test suite (xUnit + NUnit + TUnit sample projects)
./build.cmd Test
```

Or work directly with the solution at `src/Alba.sln` from your IDE or `dotnet` CLI. The main test
project is `src/Alba.Testing`.

Package versions are managed centrally in `src/Directory.Packages.props`.

## Documentation

The docs are a VitePress site under `docs/`, with code samples embedded from the test projects via
[MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets) `#region sample_*` markers. To run
the docs locally:

```bash
npm install
npm run docs
```

If you change a `sample_*` region in code, the corresponding docs page updates when the snippets
tool runs.

## Pull requests

- Open an issue first for anything beyond a small fix so the approach can be discussed.
- Add or update tests for behavior changes — `src/Alba.Testing` has acceptance-style coverage for
most features.
- Breaking changes should include an entry in the current major-version changelog file (e.g.
`v9_CHANGELOG.md`) with a short migration snippet.
- Target the `master` branch.

## Getting help

Questions are welcome on the [JasperFx Discord](https://discord.gg/WMxrvegf8H) or via GitHub
discussions/issues.
8 changes: 6 additions & 2 deletions build/_build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace></RootNamespace>
<NoWarn>CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006</NoWarn>
<NukeRootDirectory>..</NukeRootDirectory>
Expand All @@ -12,7 +12,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Nuke.Common" Version="9.0.4" />
<PackageReference Include="Nuke.Common" Version="10.1.0" />
<!-- Transitive dependencies of Nuke.Common with known vulnerabilities
in the versions it brings in (NU1901/NU1903) -->
<PackageReference Include="NuGet.Packaging" Version="7.6.0" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.9" />
</ItemGroup>

</Project>
6 changes: 5 additions & 1 deletion build/build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ class Build : NukeBuild
{
DotNetTest(_ => _
.SetProjectFile(Solution.Alba_Testing));

DotNetTest(_ => _
.SetProjectFile(Solution.NUnitSamples));

// TUnit runs on Microsoft.Testing.Platform, which the VSTest-based
// dotnet test no longer supports on the .NET 10 SDK
DotNet($"run --project {Solution.TUnitSamples.Path}");
});

Target NugetPack => _ => _
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ function getScenarioSidebar() {
{ text: 'HTTP Headers', link: '/scenarios/headers' },
{ text: 'JSON Web Services', link: '/scenarios/json' },
{ text: 'Plain Text Web Services', link: '/scenarios/text' },
{ text: 'Server-Sent Events', link: '/scenarios/sse' },
{ text: 'Xml Web Services', link: '/scenarios/xml' },
{ text: 'Sending Form Data', link: '/scenarios/formdata' },
{ text: 'Before and After Actions', link: '/scenarios/setup' },
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/bootstrapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public async Task build_host_from_Program()
// Bootstrap your application just as your real application does
var hostBuilder = Program.CreateHostBuilder(Array.Empty<string>());

await using var host = new AlbaHost(hostBuilder);
await using var host = await AlbaHost.For(hostBuilder);

// Just as a sample, I'll run a scenario against
// a "hello, world" application's root url
Expand All @@ -31,8 +31,8 @@ public async Task build_host_from_Program()
<!-- endSnippet -->

::: tip
There are both synchronous and asynchronous methods to bootstrap an `AlbaHost`. Depending on your test harness, I recommend using
the asynchronous version whenever applicable.
Bootstrapping an `AlbaHost` is asynchronous. Use your test framework's asynchronous
initialization support (e.g. xUnit's `IAsyncLifetime`) to start the host.
:::

Or alternatively, you can use one of the Alba extension methods off of `IHostBuilder` to start an `AlbaHost` object in a fluent interface
Expand Down
140 changes: 133 additions & 7 deletions docs/guide/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,48 @@ public interface IAlbaExtension : IDisposable, IAsyncDisposable
/// <summary>
/// Called during the initialization of an AlbaHost after the application is started,
/// so the application DI container is available. Useful for registering setup or teardown
/// actions on an AlbaHOst
/// actions on an AlbaHost
/// </summary>
/// <param name="host"></param>
/// <returns></returns>
Task Start(IAlbaHost host);

/// <summary>
/// Allow an extension to alter the application's
/// IHostBuilder prior to starting the application
/// Allow an extension to alter the application under test before it starts.
/// Behaves identically for every AlbaHost bootstrapping style.
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
IHostBuilder Configure(IHostBuilder builder);
void Configure(IAlbaHostBuilder builder)
{
}
}

/// <summary>
/// Hosting-model-agnostic configuration surface handed to Alba extensions
/// before the application under test starts
/// </summary>
public interface IAlbaHostBuilder
{
/// <summary>
/// Add or replace services in the application under test
/// </summary>
void ConfigureServices(Action<IServiceCollection> configure);

/// <summary>
/// Add configuration sources for the application under test. Sources added
/// here take precedence over the application's own configuration.
/// </summary>
void ConfigureConfiguration(Action<IConfigurationBuilder> configure);
}
```
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba/IAlbaExtension.cs#L5-L30' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_IAlbaExtension' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba/IAlbaExtension.cs#L6-L50' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_IAlbaExtension' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

`Configure` runs before the application under test starts and behaves the same regardless of how the
`AlbaHost` was bootstrapped (`IHostBuilder`, `WebApplicationBuilder`, or `WebApplicationFactory`). It
has a default no-op implementation, so extensions that only need post-start logic can implement
`Start` alone.

When you are initializing an `AlbaHost`, you can pass in an optional array of extensions like this sample from the security stub
testing:

Expand Down Expand Up @@ -72,3 +96,105 @@ var host = await AlbaHost.For<WebAppSecuredWithJwt.Program>(builder =>
```
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba.Testing/Samples/Extensions.cs#L8-L22' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuration_extension' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Time Travel with TimeProviderOverride

Time-sensitive endpoints — token or session expiry, cache TTLs, scheduling, "valid until" business
rules — need a deterministic clock. The `TimeProviderOverride` extension replaces the application's
`TimeProvider` registration with a controllable [FakeTimeProvider](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.time.testing.faketimeprovider)
on every bootstrapping style. The extension *is* the clock: pass it to `AlbaHost.For(...)`, then
drive time from the test.

<!-- snippet: sample_time_provider_override -->
<a id='snippet-sample_time_provider_override'></a>
```cs
[Fact]
public async Task freezing_and_advancing_the_clock()
{
// The clock is both the extension and the controllable TimeProvider
var clock = new TimeProviderOverride(new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.Zero));

await using var host = await AlbaHost.For<MinimalApiWithOakton.Program>(clock);

// Time is frozen at the configured start instant
(await host.GetAsJson<CurrentTime>("/time"))!.UtcNow
.ShouldBe(new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.Zero));
(await host.GetAsJson<CurrentTime>("/time"))!.UtcNow
.ShouldBe(new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.Zero));

// Travel forward three days
clock.Advance(TimeSpan.FromDays(3));
(await host.GetAsJson<CurrentTime>("/time"))!.UtcNow
.ShouldBe(new DateTimeOffset(2030, 1, 4, 0, 0, 0, TimeSpan.Zero));

// Or pin the clock to any instant
clock.SetUtcNow(new DateTimeOffset(2031, 6, 15, 12, 0, 0, TimeSpan.Zero));
(await host.GetAsJson<CurrentTime>("/time"))!.UtcNow
.ShouldBe(new DateTimeOffset(2031, 6, 15, 12, 0, 0, TimeSpan.Zero));
}
```
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba.Testing/time_provider_override_usage.cs#L15-L40' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_time_provider_override' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The clock is frozen: `GetUtcNow()` returns the same instant until the test moves it with
`Advance(...)` or `SetUtcNow(...)`, or opts into automatic movement via `AutoAdvanceAmount`. Timers
created from the provider (including `Task.Delay(..., timeProvider)` and cache expirations) fire
when `Advance` crosses their due time.

The override only affects code that resolves `TimeProvider` from the container. Calls to
`DateTime.UtcNow` or a directly captured `TimeProvider.System` are not intercepted — that is
inherent to the `TimeProvider` pattern, not specific to Alba.

The extension itself doubles as a compact example of the extension model:

<!-- snippet: sample_TimeProviderOverride -->
<a id='snippet-sample_TimeProviderOverride'></a>
```cs
/// <summary>
/// Alba extension that replaces the application's <see cref="TimeProvider"/> registration
/// with this controllable <see cref="FakeTimeProvider"/> for time-travel testing. Pass an
/// instance to AlbaHost.For(...), then drive time from the test with Advance(...) and
/// SetUtcNow(...). Time is frozen unless AutoAdvanceAmount is set.
/// </summary>
public sealed class TimeProviderOverride : FakeTimeProvider, IAlbaExtension
{
/// <summary>
/// Starts the clock at FakeTimeProvider's default of 2000-01-01T00:00:00Z
/// </summary>
public TimeProviderOverride()
{
}

/// <summary>
/// Starts the clock at the supplied instant
/// </summary>
public TimeProviderOverride(DateTimeOffset startDateTime) : base(startDateTime)
{
}

void IAlbaExtension.Configure(IAlbaHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.RemoveAll<TimeProvider>();
services.AddSingleton<TimeProvider>(this);
});
}

Task IAlbaExtension.Start(IAlbaHost host)
{
return Task.CompletedTask;
}

void IDisposable.Dispose()
{
}

ValueTask IAsyncDisposable.DisposeAsync()
{
return ValueTask.CompletedTask;
}
}
```
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba/TimeProviderOverride.cs#L7-L55' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_TimeProviderOverride' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
2 changes: 1 addition & 1 deletion docs/guide/gettingstarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ built in [ASP.NET Core TestServer](https://docs.microsoft.com/en-us/aspnet/core/
You can certainly write integration tests by hand using the lower level `TestServer` and `HttpClient`, but you'll write much less code with Alba. Moreover, Alba *scenarios* were meant to be declarative to maximize the readability of the integration tests, making those tests much more valuable as living technical documentation.

::: tip
As of 8.0+, Alba only supports .NET 8.0 or greater. You can still use older versions of Alba to test previous versions of ASP.NET Core.
As of 9.0+, Alba only supports .NET 10.0 or greater. You can still use older versions of Alba to test previous versions of ASP.NET Core.
:::

## Alba Setup
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ features:
- icon: 🔓
title: Authorization Stubbing
details: Stop fighting with your authorization system. Modify the shape of your user at the test level.
footer: MIT Licensed | Copyright © Jeremy D. Miller and contributors.
footer: Apache 2.0 Licensed | Copyright © Jeremy D. Miller and contributors.
---
5 changes: 2 additions & 3 deletions docs/scenarios/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ public Task using_scenario_with_ContentShouldContain_declaration_happy_path()
{
router.Handlers["/one"] = c =>
{
c.Response.Write("**just the marker**");
return Task.CompletedTask;
return c.Response.WriteAsync("**just the marker**");
};

return host.Scenario(x =>
Expand All @@ -93,5 +92,5 @@ public Task using_scenario_with_ContentShouldContain_declaration_happy_path()
});
}
```
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba.Testing/Acceptance/asserting_against_the_response_body_text.cs#L7-L23' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_ContentShouldBe' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba.Testing/Acceptance/asserting_against_the_response_body_text.cs#L8-L23' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_ContentShouldBe' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
19 changes: 18 additions & 1 deletion docs/scenarios/json.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Sending and Checking Json

JSON serialization is done with the configured input and output formatters within the underlying application. This means that
Alba can support systems using both System.Text.Json and Newtonsoft.Json.
Alba can support systems using both System.Text.Json and Newtonsoft.Json. Alba picks the application's System.Text.Json or
Newtonsoft.Json formatters even when other registered formatters (such as ASP.NET Core OData's) also advertise
`application/json`, so the JSON helpers work in applications that mix OData and plain API controllers.

## Sending Json

Expand Down Expand Up @@ -47,6 +49,21 @@ public async Task send_json_minimal_api(IAlbaHost host)
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba.Testing/Samples/JsonAndXml.cs#L7-L34' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_sending_json' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Sending Raw Json

When you already have the JSON payload as a string — malformed-input tests, captured payloads,
and the like — skip serialization and write it directly:

```cs
await host.Scenario(_ =>
{
_.Post.RawJson("{\"name\": \"Max\", \"age\": 13}").ToUrl("/person");
});
```

This writes the string to the request body verbatim and sets the `content-type` header to
`application/json`.


## Reading Json

Expand Down
Loading
Loading