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
6 changes: 4 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ updates:
- package-ecosystem: "github-actions"
directories: [".github/workflows", ".github/actions/**"]
schedule:
interval: "daily"
# interval: "daily"
interval: "weekly"
groups:
github-actions:
patterns:
Expand All @@ -17,7 +18,8 @@ updates:
- package-ecosystem: "nuget"
directory: "/" # Location of solution/project files
schedule:
interval: "daily"
# interval: "daily"
interval: "weekly"
groups:
nuget:
patterns:
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ name: Test

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
branches:
- '**'
# branches: [ main ]
# pull_request:
# branches: [ main ]

permissions:
contents: read
Expand Down
3 changes: 3 additions & 0 deletions FEFF.TestFixtures.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@
<Project Path="src/FEFF.TestFixtures.Engine/FEFF.TestFixtures.Engine.csproj" />
<Project Path="src/FEFF.TestFixtures.TUnit/FEFF.TestFixtures.TUnit.csproj" />
<Project Path="src/FEFF.TestFixtures.XunitV3/FEFF.TestFixtures.XunitV3.csproj" />
<Project Path="src/FEFF.TestFixtures.XunitV4/FEFF.TestFixtures.XunitV4.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/FEFF.TestFixtures.ApiVerification.Tests/FEFF.TestFixtures.ApiVerification.Tests.csproj" />
<Project Path="tests/FEFF.TestFixtures.Tests/FEFF.TestFixtures.Tests.csproj" />
<Project Path="tests/FEFF.TestFixtures.XunitV3.Tests/FEFF.TestFixtures.XunitV3.Tests.csproj" />
<Project Path="tests/FEFF.TestFixtures.TUnit.Tests/FEFF.TestFixtures.TUnit.Tests.csproj" />
<Project Path="tests/FEFF.TestFixtures.XunitV4.Tests/FEFF.TestFixtures.XunitV4.Tests.csproj" />
</Folder>
<Folder Name="/tests/Subjects/">
<Project Path="tests/Subjects/RefFixtures/RefFixtures.csproj" />
<Project Path="tests/Subjects/RefRefFixtures/RefRefFixtures.csproj" />
<Project Path="tests/Subjects/TUnit.TestSubject/TUnit.TestSubject.csproj" />
<Project Path="tests/Subjects/WebApiTestSubject/WebApiTestSubject.csproj" />
<Project Path="tests/Subjects/XunitV4.TestSubject/XunitV4.TestSubject.csproj" />
</Folder>
</Solution>
12 changes: 12 additions & 0 deletions NuGet.Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org"
value="https://api.nuget.org/v3/index.json"
protocolVersion="3" />
<add key="feedz.io/xunit/xunit"
value="https://f.feedz.io/xunit/xunit/nuget/index.json"
protocolVersion="3" />
</packageSources>
</configuration>
2 changes: 1 addition & 1 deletion docs/articles/02.advanced/advanced-fixture-registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ This approach gives you full control over how the fixture and its dependencies a

Fixtures that implement `IFixtureRegistrar` can expose configuration options through the `Microsoft.Extensions.Options` pattern. See the dedicated article for a complete walkthrough:

👉 [Configuring Fixtures](configuring-fixtures.md) - Learn how to add configuration support to fixtures like `TmpDirectoryFixture`.
👉 [Options Pattern](configuring-fixtures/options-pattern.md) - Learn how to add configuration support to fixtures like `TmpDirectoryFixture`.

## When to Use Advanced Registration

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Configuring Fixtures
# Options Pattern

Fixtures that implement `IFixtureRegistrar` can expose configuration options through the standard `Microsoft.Extensions.Options` pattern. This article demonstrates how to add configuration support to a fixture using `TmpDirectoryFixture` as an example.

Expand All @@ -18,41 +18,41 @@ The `TmpDirectoryFixture` provides a unique temporary directory for each test sc
Create an `Options` class and any supporting enums:

```csharp
/// <summary>
/// Specifies the behavior when the fixture is disposed.
/// </summary>
public enum DisposeType
{
/// <summary>
/// Specifies the behavior when the fixture is disposed.
/// Deletes the temporary directory and its contents on disposal.
/// </summary>
public enum DisposeType
{
/// <summary>
/// Deletes the temporary directory and its contents on disposal.
/// </summary>
Delete,

/// <summary>
/// Skips deletion of the temporary directory on disposal.
/// </summary>
/// <remarks>
/// Can be used for optimization in CI environments.
/// </remarks>
Skip
}
Delete,

/// <summary>
/// Configuration options for TmpDirectoryFixture.
/// Skips deletion of the temporary directory on disposal.
/// </summary>
public class Options
{
/// <summary>
/// Gets or sets whether the temporary directory should be deleted on disposal.
/// Defaults to DisposeType.Delete.
/// </summary>
public DisposeType DisposeType { get; set; } = DisposeType.Delete;

/// <summary>
/// Gets or sets the prefix for the temporary directory name.
/// </summary>
public string? Prefix { get; set; }
}
/// <remarks>
/// Can be used for optimization in CI environments.
/// </remarks>
Skip
}

/// <summary>
/// Configuration options for TmpDirectoryFixture.
/// </summary>
public class Options
{
/// <summary>
/// Gets or sets whether the temporary directory should be deleted on disposal.
/// Defaults to DisposeType.Delete.
/// </summary>
public DisposeType DisposeType { get; set; } = DisposeType.Delete;

/// <summary>
/// Gets or sets the prefix for the temporary directory name.
/// </summary>
public string? Prefix { get; set; }
}
```

### Step 2: Register Options with DI
Expand Down Expand Up @@ -89,27 +89,26 @@ This enables:
Inject `IOptions<Options>` into the fixture constructor:

```csharp
private readonly Options _opts;
public string Path { get; }
private readonly Options _opts;
public string Path { get; }

public TmpDirectoryFixture(IOptions<Options> opts)
{
_opts = opts.Value;
Path = Directory.CreateTempSubdirectory(_opts.Prefix).FullName;
}

public TmpDirectoryFixture(IOptions<Options> opts)
public void Dispose()
{
if (_opts.DisposeType != DisposeType.Delete)
return;

try
{
_opts = opts.Value;
Path = Directory.CreateTempSubdirectory(_opts.Prefix).FullName;
Directory.Delete(Path, true);
}

public void Dispose()
catch (DirectoryNotFoundException)
{
if (_opts.DisposeType != DisposeType.Delete)
return;

try
{
Directory.Delete(Path, true);
}
catch (DirectoryNotFoundException)
{
}
}
}
```
Expand Down Expand Up @@ -162,3 +161,5 @@ var manager = new FixtureManagerBuilder()
| [TmpDirectoryFixture.cs](https://github.com/metacoder-feff/FEFF.TestFixtures/blob/main/src/FEFF.TestFixtures/Fixtures/TmpDirectoryFixture.cs) | Complete source code example |
| [TmpDirectoryFixtureTests.cs](https://github.com/metacoder-feff/FEFF.TestFixtures/blob/main/tests/FEFF.TestFixtures.Tests/Fixtures/TmpDirectoryFixtureTests.cs) | Unit tests for TmpDirectoryFixture |
| [OptionsConfigurationTests.cs](https://github.com/metacoder-feff/FEFF.TestFixtures/blob/main/tests/FEFF.TestFixtures.Tests/EngineTests/OptionsConfigurationTests.cs) | Tests for options configuration patterns |
| [Parameterization Pattern](parameterization-pattern.md) | Alternative generic type-based configuration |
| [Selecting Configuration Method](selecting-configuration-method.md) | Comparison of configuration approaches |
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# Fixture Parameterization

Fixture parameterization allows you to create fixture instances with concrete argument values by defining options types that supply configuration data. This pattern is essential when you need the same fixture to behave differently across tests based on custom parameters.
# Generic Parameterization Pattern

## Overview

While standard fixtures are created with default behavior, parameterized fixtures accept configuration through a dedicated options type. This options type is itself a fixture, enabling dependency injection and test isolation.
Fixture parameterization lets you pass values to a fixture that are required but not known when the fixture is created. Instead of hardcoding configuration inside the fixture, you supply it from the outside.

This is useful when:

The pattern consists of three components:
- Different tests need the same fixture to behave differently
- Configuration values (connection strings, endpoints, flags) are decided at test time, not when the fixture is written
- The same fixture is reused across test suites with different settings

1. **Options Interface** - Defines the contract for configuration data
2. **Options Fixture** - Implements the interface with concrete values
3. **Parameterized Fixture** - Consumes the options fixture via constructor injection of generic type parameter
The pattern uses three pieces:

1. **Options Interface** — defines what values the fixture needs
2. **Options Fixture** — provides the actual values
3. **Parameterized Fixture** — receives the values through its constructor

## Example: TmpDatabaseNameFixture

Expand Down Expand Up @@ -66,8 +70,8 @@ public class OptionsFixture : ITmpDatabaseNameFixtureOptions
```

The options fixture can:
- Return hardcoded values
- Read from environment variables

- Return hardcoded or calculated values
- Depend on other fixtures for dynamic configuration

### Step 4: Use the Parameterized Fixture
Expand All @@ -82,7 +86,7 @@ var fixtureInstance = TestContext.Current.GetFeffFixture<TmpDatabaseNameFixture<
The generic type parameter `OptionsFixture` tells `TmpDatabaseNameFixture` which options to use.

> [!TIP]
> Single `OptionsFixture` class can implement multiple different interfaces to parameterize multiple fixtures used in a test suite.
> A single `OptionsFixture` class can implement multiple different interfaces to parameterize multiple fixtures used in a test suite.

## Benefits of Fixture Parameterization

Expand All @@ -95,8 +99,8 @@ The generic type parameter `OptionsFixture` tells `TmpDatabaseNameFixture` which

| Resource | Description |
|----------|-------------|
| [TmpDatabaseNameFixture.cs](https://github.com/metacoder-feff/FEFF.TestFixtures/blob/main/src/FEFF.TestFixtures.AspNetCore/Fixtures/TmpDatabaseNameFixture.cs) | Parameterized fixture implementation |
| [ITmpDatabaseNameFixtureOptions.cs](https://github.com/metacoder-feff/FEFF.TestFixtures/blob/main/src/FEFF.TestFixtures.AspNetCore/Fixtures/TmpDatabaseNameFixture.cs) | Options interface definition |
| [TmpDatabaseNameFixture.cs](https://github.com/metacoder-feff/FEFF.TestFixtures/blob/main/src/FEFF.TestFixtures.AspNetCore/Fixtures/TmpDatabaseNameFixture.cs) | Parameterized fixture and options interface |
| [ApiTests.cs](https://github.com/metacoder-feff/FEFF.TestFixtures/blob/main/examples/ExampleTests.AspNetCore/ApiTests.cs) | Example usage in API tests |
| [TmpDatabaseNameFixtureTests.cs](https://github.com/metacoder-feff/FEFF.TestFixtures/blob/main/tests/FEFF.TestFixtures.Tests/FixturesTests/FEFF.TestFixtures.AspNetCore/TmpDatabaseNameFixtureTests.cs) | Unit tests for parameterization |
| [Configuring Fixtures](configuring-fixtures.md) | Alternative configuration via environment variables |
| [Options Pattern](options-pattern.md) | Alternative configuration via environment variables |
| [Selecting Configuration Method](selecting-configuration-method.md) | Comparison with Options Pattern |
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Selecting Configuration Method

The library provides two distinct ways to configure fixtures. Choose based on whether the configuration varies between tests or is controlled externally.

| Aspect | [Parameterization Pattern](parameterization-pattern.md) | [Options Pattern](options-pattern.md) |
|--------|----------------------------------------|---------------------------------------------------------------------|
| **When to use** | Different tests or test suites need different behavior for the same fixture | Global or environment-specific settings (CI, debugging, local overrides) |
| **Value requirement** | Fixture explicitly requires a value | Fixture should have default value |
| **Mechanism** | Generic type parameter + options fixture interface | `Microsoft.Extensions.Options` bound to configuration (e.g., environment variables) |
| **Discovery** | Compile-time via generic constraint | Runtime via `IOptions<T>` and `IFixtureRegistrar` |
| **Flexibility** | Per-test-suite variation by swapping the options fixture type | Global change without recompiling tests |
| **Example scenario** | One test suite uses database `A`, another uses database `B`, via `TmpDatabaseNameFixture<SuiteAOptions>` vs `TmpDatabaseNameFixture<SuiteBOptions>` | Skipping temp directory cleanup on CI by setting `TmpDirectoryFixture__DisposeType=Skip` |

## Decision Guide

- **Use parameterization** when the fixture must behave differently across test classes or collections and the decision is made at design time. This keeps configuration type-safe and visible in test code.
- **Use the options pattern** when you need operators or CI pipelines to adjust behavior without changing source code, or when a setting applies broadly across all tests using that fixture.

The two patterns can coexist: a fixture may accept an options fixture for structural choices (which connection strings to redirect) while also using `IOptions<T>` for operational tuning (cleanup behavior, timeouts).

## See Also

| Resource | Description |
|----------|-------------|
| [Parameterization Pattern](parameterization-pattern.md) | Generic type-based configuration |
| [Options Pattern](options-pattern.md) | Environment variable-based configuration |
10 changes: 7 additions & 3 deletions docs/articles/02.advanced/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
- name: Advanced Fixture Registration
href: advanced-fixture-registration.md
- name: Configuring Fixtures
href: configuring-fixtures.md
- name: Fixture Parameterization
href: fixture-parameterization.md
items:
- name: Options Pattern
href: configuring-fixtures/options-pattern.md
- name: Generic Parameterization Pattern
href: configuring-fixtures/parameterization-pattern.md
- name: Selecting Configuration Method
href: configuring-fixtures/selecting-configuration-method.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<InternalsVisibleTo Include="FEFF.TestFixtures.Engine" />
<InternalsVisibleTo Include="FEFF.TestFixtures.AspNetCore" />
<InternalsVisibleTo Include="FEFF.TestFixtures.XunitV3" />
<InternalsVisibleTo Include="FEFF.TestFixtures.XunitV4" />
<InternalsVisibleTo Include="FEFF.TestFixtures.TUnit" />
</ItemGroup>

Expand Down
10 changes: 7 additions & 3 deletions src/FEFF.TestFixtures.Abstractions/Utils/ThrowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,21 @@ public static void Assert(
/// Returns argument if it is not null.<br/>
/// Throws <see cref="InvalidOperationException"/> otherwise.
/// </summary>
/// <typeparam name="T">The type of the argument.</typeparam>
/// <param name="argument">The value to check for null.</param>
/// <param name="paramName">The name of the parameter (auto-populated).</param>
/// <returns>The argument if not null.</returns>
/// <remarks>
/// It is convenient to wrap a complex expression with <see cref="EnsureNotNull"/>:
/// <example>
/// <code>
/// var result = ThrowHelper.Ensure(anObject?.Nested?.Nested?.Value);
/// var result = ThrowHelper.EnsureNotNull(anObject?.Nested?.Nested?.Value);
/// </code>
/// </example>
/// Here:
/// <list type="number">
/// <item> The whole expression is serialized into error message, if expression is null.</item>
/// <item> The expression is calculated only once and is returned to consumer, if expression is NOT null.</item>
/// <item>The whole expression is serialized into error message, if expression is null.</item>
/// <item>The expression is calculated only once and is returned to consumer, if expression is NOT null.</item>
/// </list>
/// </remarks>
/// <exception cref="InvalidOperationException"></exception>
Expand Down
11 changes: 9 additions & 2 deletions src/FEFF.TestFixtures.AspNetCore.EF/DatabaseLifecycleFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public interface IDatabaseLifecycleFixture<TContext> : IDatabaseLifecycleFixture
/// Gets the <typeparamref name="TContext" /> instance resolved from the service provider.
/// </summary>
/// <remarks>
/// Starts the application under test if not already running.
/// Accessing this property starts the application under test if not already running.
/// </remarks>
TContext LazyDbContext { get; }
}
Expand All @@ -39,8 +39,11 @@ public interface IDatabaseLifecycleFixture<TContext> : IDatabaseLifecycleFixture
/// A fixture that manages Entity Framework Core database creation and deletion
/// for the lifetime of a test scope. The database is deleted on <see cref="DisposeAsync"/>.
/// </summary>
/// <remarks>
/// Note: EnsureDeleted may fail for admin databases. Consider using TmpDbNameFixture for such cases.
/// </remarks>
/// <typeparam name="TEntryPoint">The application entry point type.</typeparam>
/// <typeparam name="TContext">The <see cref="LazyDbContext"/> type to manage.</typeparam>
/// <typeparam name="TContext">The <see cref="DbContext"/> type to manage.</typeparam>
[Fixture]
public sealed class DatabaseLifecycleFixture<TEntryPoint, TContext> : IAsyncDisposable, IDatabaseLifecycleFixture<TContext>
where TEntryPoint : class
Expand Down Expand Up @@ -70,6 +73,10 @@ public async ValueTask DisposeAsync()
return;

await LazyDbContext.Database.EnsureDeletedAsync().ConfigureAwait(false);
// TODO:
// Log warning but do not throw to prevent test failures during cleanup
// This can happen when database is locked or inaccessible
// System.Console.Error.WriteLine($"[DatabaseLifecycleFixture] Warning: Failed to delete database: {ex.Message}");
}

/// <inheritdoc/>
Expand Down
Loading
Loading