Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4.2.0 #70

Merged
merged 6 commits into from
Mar 7, 2024
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Represents the **NuGet** versions.

## v4.2.0
- *Enhancement:* Any configuration specified as part of registering the `HttpClient` services from a Dependency Injection (DI) perspective is ignored by default when creating an `HttpClient` using the `MockHttpClientFactory`. This default behavior is intended to potentially minimize any side-effect behavior that may occur that is not intended for the unit testing. See [`README`](./README.md#http-client-configurations) for more details on capabilities introduced; highlights are:
- New `MockHttpClient.WithConfigurations` method indicates that the `HttpMessageHandler` and `HttpClient` configurations are to be used.
- New `MockHttpClient.WithoutMocking` method indicates that the underlying `HttpClient` is **not** to be mocked; i.e. will result in an actual/real HTTP request to the specified endpoint. This will allow the mixing of real and mocked HTTP requests within the same test.

## v4.1.2
- *Fixed*: The `AssertLocationHeader` has been corrected to also support the specification of the `Uri` as a string. Additionally, contains support has been added with `AssertLocationHeaderContains`.

Expand Down
2 changes: 1 addition & 1 deletion Common.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>4.1.2</Version>
<Version>4.2.0</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,46 @@ test.ReplaceHttpClientFactory(mcf)
.Assert(new { id = "Abc", description = "A blue carrot" });
```

</br>
<br/>

### HTTP Client configurations

Any configuration specified as part of the registering the `HttpClient` services from a Dependency Injection (DI) perspective is ignored by default when creating an `HttpClient` using the `MockHttpClientFactory`. This default behavior is intended to potentially minimize any side-effect behavior that may occur that is not intended for the unit testing. For example, a `DelegatingHandler` may be configured that requests a token from an identity provider which is not needed for the unit test, or may fail due to lack of access from the unit testing environment.

``` csharp
// Startup service (DI) configuration.
services.AddHttpClient("XXX", hc => hc.BaseAddress = new System.Uri("https://somesys")) // This is HttpClient configuration.
.AddHttpMessageHandler(_ => new MessageProcessingHandler()) // This is HttpMessageHandler configuration.
.ConfigureHttpClient(hc => hc.DefaultRequestVersion = new Version(1, 2)); // This is further HttpClient configuration.
```

However, where the configuration is required then the `MockHttpClient` can be configured _explicitly_ to include the configuration; the following methods enable:

Method | Description
- | -
`WithConfigurations` | Indicates that the `HttpMessageHandler` and `HttpClient` configurations are to be used. *
`WithoutConfigurations` | Indicates that the `HttpMessageHandler` and `HttpClient` configurations are _not_ to be used (this is the default state).
`WithHttpMessageHandlers` | Indicates that the `HttpMessageHandler` configurations are to be used. *
`WithoutHttpMessageHandlers` | Indicates that the `HttpMessageHandler` configurations are _not_ to be used.
`WithHttpClientConfigurations` | Indicates that the `HttpClient` configurations are to be used.
`WithoutHttpClientConfigurations` | Indicates that the `HttpClient` configurations are to be used.
-- | --
`WithoutMocking` | Indicates that the underlying `HttpClient` is **not** to be mocked; i.e. will result in an actual/real HTTP request to the specified endpoint. This is useful to achieve a level of testing where both mocked and real requests are required. Note that an `HttpClient` cannot support both, these would need to be tested separately.

_Note:_ `*` above denotes that an array of `DelegatingHandler` types to be excluded can be specified; with the remainder being included within the order specified.

``` csharp
// Mock with configurations.
var mcf = MockHttpClientFactory.Create();
mcf.CreateClient("XXX").WithConfigurations()
.Request(HttpMethod.Get, "products/xyz").Respond.With(HttpStatusCode.NotFound);

// No mocking, real request.
var mcf = MockHttpClientFactory.Create();
mcf.CreateClient("XXX").WithoutMocking();
```

<br/>

### Times

Expand Down
2 changes: 1 addition & 1 deletion src/UnitTestEx.MSTest/UnitTestEx.MSTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/UnitTestEx.NUnit/UnitTestEx.NUnit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NUnit" Version="4.0.1" />
<PackageReference Include="NUnit" Version="4.1.0" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/UnitTestEx.Xunit/UnitTestEx.Xunit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="xunit" Version="2.6.5" />
<PackageReference Include="xunit" Version="2.7.0" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 2 additions & 1 deletion src/UnitTestEx/AspNetCore/HttpTesterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using UnitTestEx.Abstractions;
using UnitTestEx.Assertors;
using UnitTestEx.Json;
using UnitTestEx.Mocking;

namespace UnitTestEx.AspNetCore
{
Expand Down Expand Up @@ -204,7 +205,7 @@ public async Task<HttpResponseMessage> SendAsync(HttpMethod method, string? requ
private static HttpRequestMessage CreateRequest(HttpMethod method, string requestUri, HttpContent? content, Action<HttpRequestMessage>? requestModifier)
{
var uri = new Uri(requestUri, UriKind.RelativeOrAbsolute);
var ub = new UriBuilder(uri.IsAbsoluteUri ? uri : new Uri(new Uri("https://unittestex"), requestUri));
var ub = new UriBuilder(uri.IsAbsoluteUri ? uri : new Uri(MockHttpClient.DefaultBaseAddress, requestUri));

var request = new HttpRequestMessage(method, ub.Uri);
if (content != null)
Expand Down
30 changes: 9 additions & 21 deletions src/UnitTestEx/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,8 @@ public static class ExtensionMethods
/// <remarks>The <see cref="IServiceCollection"/> to support fluent-style method-chaining.</remarks>
public static IServiceCollection ReplaceSingleton<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class
{
if (services == null)
throw new ArgumentNullException(nameof(services));

if (implementationFactory == null)
throw new ArgumentNullException(nameof(implementationFactory));
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(implementationFactory);

services.Remove<TService>();
return services.AddSingleton(implementationFactory);
Expand All @@ -59,8 +56,7 @@ public static IServiceCollection ReplaceSingleton<TService>(this IServiceCollect
/// <remarks>The <see cref="IServiceCollection"/> to support fluent-style method-chaining.</remarks>
public static IServiceCollection ReplaceSingleton<TService, TImplementation>(this IServiceCollection services) where TService : class where TImplementation : class, TService
{
if (services == null)
throw new ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

services.Remove<TService>();
return services.AddSingleton<TService, TImplementation>();
Expand All @@ -79,11 +75,8 @@ public static IServiceCollection ReplaceSingleton<TService, TImplementation>(thi
/// <remarks>The <see cref="IServiceCollection"/> to support fluent-style method-chaining.</remarks>
public static IServiceCollection ReplaceScoped<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class
{
if (services == null)
throw new ArgumentNullException(nameof(services));

if (implementationFactory == null)
throw new ArgumentNullException(nameof(implementationFactory));
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(implementationFactory);

services.Remove<TService>();
return services.AddScoped(implementationFactory);
Expand All @@ -107,8 +100,7 @@ public static IServiceCollection ReplaceScoped<TService>(this IServiceCollection
/// <remarks>The <see cref="IServiceCollection"/> to support fluent-style method-chaining.</remarks>
public static IServiceCollection ReplaceScoped<TService, TImplementation>(this IServiceCollection services) where TService : class where TImplementation : class, TService
{
if (services == null)
throw new ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

services.Remove<TService>();
return services.AddScoped<TService, TImplementation>();
Expand All @@ -127,11 +119,8 @@ public static IServiceCollection ReplaceScoped<TService, TImplementation>(this I
/// <remarks>The <see cref="IServiceCollection"/> to support fluent-style method-chaining.</remarks>
public static IServiceCollection ReplaceTransient<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class
{
if (services == null)
throw new ArgumentNullException(nameof(services));

if (implementationFactory == null)
throw new ArgumentNullException(nameof(implementationFactory));
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(implementationFactory);

services.Remove<TService>();
return services.AddTransient(implementationFactory);
Expand All @@ -155,8 +144,7 @@ public static IServiceCollection ReplaceTransient<TService>(this IServiceCollect
/// <remarks>The <see cref="IServiceCollection"/> to support fluent-style method-chaining.</remarks>
public static IServiceCollection ReplaceTransient<TService, TImplementation>(this IServiceCollection services) where TService : class where TImplementation : class, TService
{
if (services == null)
throw new ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

services.Remove<TService>();
return services.AddTransient<TService, TImplementation>();
Expand Down
30 changes: 25 additions & 5 deletions src/UnitTestEx/Json/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ namespace UnitTestEx.Json
/// <summary>
/// Provides the <see cref="Stj.JsonSerializer"/> encapsulated implementation.
/// </summary>
/// <param name="options">The <see cref="Stj.JsonSerializerOptions"/>. Defaults to <see cref="DefaultOptions"/>.</param>
public class JsonSerializer(Stj.JsonSerializerOptions? options = null) : IJsonSerializer
public class JsonSerializer : IJsonSerializer
{
private static IJsonSerializer? _default;

/// <summary>
/// Gets or sets the default <see cref="Stj.JsonSerializerOptions"/>.
/// </summary>
Expand All @@ -23,15 +24,34 @@ public class JsonSerializer(Stj.JsonSerializerOptions? options = null) : IJsonSe
/// <summary>
/// Gets or sets the default <see cref="IJsonSerializer"/>.
/// </summary>
public static IJsonSerializer Default { get; set; } = new JsonSerializer();
public static IJsonSerializer Default
{
get => _default ??= new JsonSerializer();
set => _default = value ?? throw new ArgumentNullException(nameof(value));
}

/// <summary>
/// Initializes a new instance of the <see cref="JsonSerializer"/> class.
/// </summary>
/// <param name="options">The <see cref="Stj.JsonSerializerOptions"/>. Defaults to <see cref="DefaultOptions"/>.</param>
public JsonSerializer(Stj.JsonSerializerOptions? options = null)
{
Options = options ?? DefaultOptions;
IndentedOptions = new Stj.JsonSerializerOptions(Options) { WriteIndented = true };
}

/// <inheritdoc/>
object IJsonSerializer.Options => Options;

/// <summary>
/// Gets the <see cref="Stj.JsonSerializerOptions"/>.
/// </summary>
public Stj.JsonSerializerOptions Options { get; } = options ?? DefaultOptions;
public Stj.JsonSerializerOptions Options { get; }

/// <summary>
/// Gets or sets the <see cref="Stj.JsonSerializerOptions"/> with <see cref="Stj.JsonSerializerOptions.WriteIndented"/> = <c>true</c>.
/// </summary>
public Stj.JsonSerializerOptions? IndentedOptions { get; }

/// <inheritdoc/>
public object? Deserialize(string json) => Stj.JsonSerializer.Deserialize<dynamic>(json, Options);
Expand All @@ -43,6 +63,6 @@ public class JsonSerializer(Stj.JsonSerializerOptions? options = null) : IJsonSe
public T? Deserialize<T>(string json) => Stj.JsonSerializer.Deserialize<T>(json, Options)!;

/// <inheritdoc/>
public string Serialize<T>(T value, JsonWriteFormat? format = null) => Stj.JsonSerializer.Serialize(value, format == null ? Options : new Stj.JsonSerializerOptions(Options) { WriteIndented = format.Value == JsonWriteFormat.Indented });
public string Serialize<T>(T value, JsonWriteFormat? format = null) => Stj.JsonSerializer.Serialize(value, format == null || format.Value == JsonWriteFormat.None ? Options : IndentedOptions);
}
}
3 changes: 1 addition & 2 deletions src/UnitTestEx/Logging/LoggerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
if (!IsEnabled(logLevel))
return;

if (formatter == null)
throw new ArgumentNullException(nameof(formatter));
ArgumentNullException.ThrowIfNull(formatter);

var sb = new StringBuilder();
sb.Append($"{DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffff", DateTimeFormatInfo.InvariantInfo)} {GetLogLevel(logLevel)}: {formatter(state, exception)} [{Name}]");
Expand Down
Loading
Loading