Skip to content

Commit

Permalink
v4.2.0 (#70)
Browse files Browse the repository at this point in the history
* WithConfigurations and WithoutMocking

* Additional internal tweaks.

* Finalize.

* Correct spelling.

* Fix grammar.

* Fix test to run as github action
  • Loading branch information
chullybun authored Mar 7, 2024
1 parent fde72f8 commit 72a4ab5
Show file tree
Hide file tree
Showing 26 changed files with 475 additions and 86 deletions.
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

0 comments on commit 72a4ab5

Please sign in to comment.