SweetMock.Extensions.Options
0.9.41
dotnet add package SweetMock.Extensions.Options --version 0.9.41
NuGet\Install-Package SweetMock.Extensions.Options -Version 0.9.41
<PackageReference Include="SweetMock.Extensions.Options" Version="0.9.41" />
<PackageVersion Include="SweetMock.Extensions.Options" Version="0.9.41" />
<PackageReference Include="SweetMock.Extensions.Options" />
paket add SweetMock.Extensions.Options --version 0.9.41
#r "nuget: SweetMock.Extensions.Options, 0.9.41"
#:package SweetMock.Extensions.Options@0.9.41
#addin nuget:?package=SweetMock.Extensions.Options&version=0.9.41
#tool nuget:?package=SweetMock.Extensions.Options&version=0.9.41
SweetMock.Extensions.Options
A comprehensive extension library for mocking IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T> in .NET tests using the SweetMock framework.
Features
- Mock IOptions<T>: Simple, immutable configuration mocking
- Mock IOptionsSnapshot<T>: Scoped configuration with named options support
- Mock IOptionsMonitor<T>: Dynamic configuration with change notifications
- Auto-initialization: Automatically creates default option values when possible
- Named Options: Support for multiple named configuration instances
- Modify Configuration: Update option values during tests
- Change Notifications: Test configuration change callbacks with IOptionsMonitor
Installation
dotnet add package SweetMock.Extensions.Options
Quick Start
Basic IOptions<T> Mock
using Microsoft.Extensions.Options;
[Fixture<MyService>]
[Mock<IOptions<AppSettings>, MockIOptions<AppSettings>>]
public class MyServiceTests
{
[Fact]
public void Service_UsesConfiguredOptions()
{
// Arrange
var settings = new AppSettings { ConnectionString = "Server=localhost" };
var fixture = Fixture.MyService(config =>
config.appSettings.Value(() => settings)
);
var sut = fixture.CreateMyService();
// Act
var result = sut.Connect();
// Assert
Assert.True(result);
}
}
public class MyService(IOptions<AppSettings> appSettings)
{
public bool Connect() => appSettings.Value.ConnectionString != null;
}
public class AppSettings
{
public string? ConnectionString { get; set; }
}
IOptions<T>
IOptions<T> provides simple, immutable configuration values. Best for configuration that doesn't change during the application lifetime.
Auto-initialization
If your options class has a parameterless constructor, it will be auto-initialized:
[Fact]
public void DefaultOptions_AreAutoCreated()
{
// Arrange
var fixture = Fixture.MyService();
var sut = fixture.CreateMyService();
// Act
var settings = sut.GetSettings();
// Assert - Uses default values from AppSettings constructor
Assert.Equal("DefaultValue", settings.Name);
}
public class AppSettings
{
public string Name { get; set; } = "DefaultValue";
public int Timeout { get; set; } = 30;
}
Setting Option Values
[Fact]
public void CustomOptions_AreUsed()
{
// Arrange
var customSettings = new AppSettings
{
Name = "CustomName",
Timeout = 60
};
var fixture = Fixture.MyService(config =>
config.settings.Value(() => customSettings)
);
var sut = fixture.CreateMyService();
// Act & Assert
Assert.Equal("CustomName", sut.GetSettings().Name);
Assert.Equal(60, sut.GetSettings().Timeout);
}
Modifying Options
[Fact]
public void Options_CanBeModified()
{
// Arrange
var fixture = Fixture.MyService();
var sut = fixture.CreateMyService();
// Act - Modify after initialization
fixture.Config.settings.Modify(s => s.Name = "Modified");
// Assert
Assert.Equal("Modified", sut.GetSettings().Name);
}
IOptionsSnapshot<T>
IOptionsSnapshot<T> provides scoped configuration values that can be reloaded. Supports named options for different configurations.
Default and Named Options
[Fixture<ConfigService>]
[Mock<IOptionsSnapshot<DatabaseSettings>, MockIOptionsSnapshot<DatabaseSettings>>]
public class ConfigServiceTests
{
[Fact]
public void NamedOptions_ReturnDifferentValues()
{
// Arrange
var fixture = Fixture.ConfigService(config =>
{
config.dbSettings.Get("Primary", new DatabaseSettings
{
ConnectionString = "Server=primary"
});
config.dbSettings.Get("Secondary", new DatabaseSettings
{
ConnectionString = "Server=secondary"
});
});
var sut = fixture.CreateConfigService();
// Act
var primary = sut.GetConnection("Primary");
var secondary = sut.GetConnection("Secondary");
// Assert
Assert.Equal("Server=primary", primary);
Assert.Equal("Server=secondary", secondary);
}
}
public class ConfigService(IOptionsSnapshot<DatabaseSettings> dbSettings)
{
public string GetConnection(string name) => dbSettings.Get(name).ConnectionString;
}
public class DatabaseSettings
{
public string ConnectionString { get; set; } = "default";
}
Initialize and Modify
[Fact]
public void Snapshot_CanBeInitializedAndModified()
{
// Arrange
var fixture = Fixture.ConfigService(config =>
config.dbSettings.Initialize(new DatabaseSettings
{
ConnectionString = "Initial"
})
);
var sut = fixture.CreateConfigService();
// Verify initial value
Assert.Equal("Initial", sut.GetDefaultConnection());
// Act - Modify during test
fixture.Config.dbSettings.Modify(s => s.ConnectionString = "Updated");
// Assert
Assert.Equal("Updated", sut.GetDefaultConnection());
}
Multiple Named Configurations
[Fact]
public void MultipleNamedOptions_AreIndependent()
{
// Arrange
var fixture = Fixture.ConfigService(config =>
{
config.dbSettings.Initialize("Config1", new DatabaseSettings());
config.dbSettings.Initialize("Config2", new DatabaseSettings());
});
var sut = fixture.CreateConfigService();
// Act - Modify only one
fixture.Config.dbSettings.Modify("Config1", s => s.ConnectionString = "Modified");
// Assert - Only Config1 changed
Assert.Equal("Modified", sut.GetConnection("Config1"));
Assert.Equal("default", sut.GetConnection("Config2"));
}
IOptionsMonitor<T>
IOptionsMonitor<T> provides dynamic configuration with change notifications. Use this when you need to test configuration reloading scenarios.
Basic Monitor Usage
[Fixture<MonitoredService>]
[Mock<IOptionsMonitor<AppConfig>, MockIOptionsMonitor<AppConfig>>]
public class MonitoredServiceTests
{
[Fact]
public void Monitor_TracksConfigChanges()
{
// Arrange
var fixture = Fixture.MonitoredService(config =>
config.appConfig.Initialize(new AppConfig { Feature = "Initial" })
);
var sut = fixture.CreateMonitoredService();
// Act
var before = sut.CurrentFeature;
fixture.Config.appConfig.Modify(c => c.Feature = "Updated");
var after = sut.CurrentFeature;
// Assert
Assert.Equal("Initial", before);
Assert.Equal("Updated", after);
}
}
public class MonitoredService(IOptionsMonitor<AppConfig> appConfig)
{
public string CurrentFeature => appConfig.CurrentValue.Feature;
}
public class AppConfig
{
public string Feature { get; set; } = "Default";
}
Change Notifications
[Fact]
public void OnChange_NotifiesSubscribers()
{
// Arrange
var changes = new List<string>();
var monitor = Mock.IOptionsMonitor<AppConfig>(out var config);
// Subscribe to changes
monitor.OnChange(settings => changes.Add(settings.Feature));
// Act
config.Modify(s => s.Feature = "Change1");
config.Modify(s => s.Feature = "Change2");
config.Modify(s => s.Feature = "Change3");
// Assert
Assert.Equal(3, changes.Count);
Assert.Equal("Change1", changes[0]);
Assert.Equal("Change2", changes[1]);
Assert.Equal("Change3", changes[2]);
}
Disposing Change Listeners
[Fact]
public void DisposedListener_StopsReceivingNotifications()
{
// Arrange
var changes = new List<string>();
var monitor = Mock.IOptionsMonitor<AppConfig>(out var config);
var listener = monitor.OnChange(settings => changes.Add(settings.Feature));
// Act
config.Modify(s => s.Feature = "Change1");
config.Modify(s => s.Feature = "Change2");
listener.Dispose(); // Stop listening
config.Modify(s => s.Feature = "Change3");
// Assert
Assert.Equal(2, changes.Count); // Only first 2 changes captured
Assert.DoesNotContain("Change3", changes);
}
Named Monitors
[Fact]
public void NamedMonitors_TrackIndependently()
{
// Arrange
var fixture = Fixture.MonitoredService(config =>
{
config.appConfig.Initialize("Service1", new AppConfig { Feature = "S1" });
config.appConfig.Initialize("Service2", new AppConfig { Feature = "S2" });
});
var sut = fixture.CreateMonitoredService();
// Act
fixture.Config.appConfig.Modify("Service1", c => c.Feature = "S1-Modified");
var service1 = sut.GetConfig("Service1");
var service2 = sut.GetConfig("Service2");
// Assert
Assert.Equal("S1-Modified", service1);
Assert.Equal("S2", service2); // Unchanged
}
Complete Example
[Fixture<EmailService>]
[Mock<IOptionsMonitor<EmailSettings>, MockIOptionsMonitor<EmailSettings>>]
public class EmailServiceTests
{
[Fact]
public void EmailService_ReactsToConfigChanges()
{
// Arrange
var sentEmails = new List<string>();
var fixture = Fixture.EmailService(config =>
config.emailSettings.Initialize(new EmailSettings
{
SmtpServer = "smtp.example.com",
Port = 587,
Enabled = true
})
);
var sut = fixture.CreateEmailService();
// Act - Send with initial config
sut.SendEmail("test@example.com", "Hello");
// Disable emails via config change
fixture.Config.emailSettings.Modify(s => s.Enabled = false);
// Try to send again
sut.SendEmail("test2@example.com", "World");
// Assert
Assert.Single(sentEmails); // Only first email sent
}
}
public class EmailService
{
private readonly IOptionsMonitor<EmailSettings> _settings;
private readonly IDisposable _changeListener;
public EmailService(IOptionsMonitor<EmailSettings> settings)
{
_settings = settings;
_changeListener = settings.OnChange(newSettings =>
{
Console.WriteLine($"Config changed: Enabled={newSettings.Enabled}");
});
}
public void SendEmail(string to, string message)
{
if (!_settings.CurrentValue.Enabled)
{
Console.WriteLine("Email disabled");
return;
}
// Send email logic...
}
}
public class EmailSettings
{
public string SmtpServer { get; set; } = "localhost";
public int Port { get; set; } = 25;
public bool Enabled { get; set; } = true;
}
API Reference
MockIOptions<TOptions>
Provides immutable configuration values.
// Auto-initialized if TOptions has parameterless constructor
var options = Mock.IOptions<AppSettings>();
// Set value explicitly
var options = Mock.IOptions<AppSettings>(config =>
config.Value(() => new AppSettings { Name = "Test" })
);
// Modify after creation
config.Modify(settings => settings.Name = "Updated");
MockIOptionsSnapshot<TOptions>
Provides scoped configuration with named options.
// Default option
config.snapshot.Initialize(new AppSettings());
// Named options
config.snapshot.Get("Config1", new AppSettings());
config.snapshot.Get("Config2", new AppSettings());
// Modify default
config.snapshot.Modify(s => s.Name = "New");
// Modify named
config.snapshot.Modify("Config1", s => s.Name = "New");
MockIOptionsMonitor<TOptions>
Provides dynamic configuration with change notifications.
// Initialize
config.monitor.Initialize(new AppSettings());
config.monitor.Initialize("Named", new AppSettings());
// Modify (triggers OnChange callbacks)
config.monitor.Modify(s => s.Name = "Updated");
config.monitor.Modify("Named", s => s.Name = "Updated");
// Subscribe to changes
var listener = monitor.OnChange((settings, name) =>
{
Console.WriteLine($"Config '{name}' changed");
});
// Unsubscribe
listener.Dispose();
Error Handling
Options That Cannot Be Auto-Created
If your options class doesn't have a parameterless constructor, you must initialize it explicitly:
// This will throw NotExplicitlyMockedException
public class RequiredParamOptions(string required);
[Fact]
public void NonAutoCreatable_ThrowsException()
{
var sut = Mock.IOptions<RequiredParamOptions>();
// Throws NotExplicitlyMockedException
Assert.Throws<NotExplicitlyMockedException>(() => sut.Value);
}
// Solution: Initialize explicitly
var fixture = Fixture.MyService(config =>
config.options.Value(() => new RequiredParamOptions("value"))
);
Unknown Named Options
Accessing unknown named options throws ArgumentException:
[Fact]
public void UnknownOption_ThrowsArgumentException()
{
var snapshot = Mock.IOptionsSnapshot<AppSettings>();
Assert.Throws<ArgumentException>(() => snapshot.Get("Unknown"));
}
Choosing the Right Interface
- IOptions<T>: Use for configuration that never changes during application lifetime (e.g., static app settings)
- IOptionsSnapshot<T>: Use for scoped configuration that can be reloaded per request/scope (e.g., per-request settings)
- IOptionsMonitor<T>: Use for singleton services that need to react to configuration changes in real-time (e.g., feature flags, dynamic settings)
Requirements
- .NET 8.0, 9.0, or 10.0
- SweetMock
- Microsoft.Extensions.Options
License
See the main SweetMock repository for license information.
Links
| 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 is compatible. 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
- Microsoft.Extensions.Options (>= 10.0.0)
- SweetMock (>= 0.9.41)
-
net8.0
- Microsoft.Extensions.Options (>= 10.0.0)
- SweetMock (>= 0.9.41)
-
net9.0
- Microsoft.Extensions.Options (>= 10.0.0)
- SweetMock (>= 0.9.41)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.