Skip to content

Commit

Permalink
Refactored logging and testing infrastructure, added new ApiTesting n…
Browse files Browse the repository at this point in the history
…amespace.

This commit includes significant changes to the testing and logging infrastructure. The improvements resulted in cleaner API for test logging, allowing multiple kinds of test loggers such as PulseFlow and SimpleTestLogger. Moreover, a new namespace, ApiTesting, was established which includes Assertion, AssertionGroup and Result classes. This will enhance the organization and readability of test cases. This commit also included relevant changes to the project and solution files.
  • Loading branch information
frankhaugen committed Jan 14, 2024
1 parent 067d3c9 commit 42a7ce7
Show file tree
Hide file tree
Showing 34 changed files with 520 additions and 171 deletions.
92 changes: 92 additions & 0 deletions Frank.Testing.ApiTesting/ApiTestingHarness.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System.Diagnostics;

namespace Frank.Testing.ApiTesting;

public class ApiTestingHarness
{
private readonly IHttpClientFactory _httpClientFactory;

public ApiTestingHarness(IHttpClientFactory httpClientFactory)

Check warning on line 9 in Frank.Testing.ApiTesting/ApiTestingHarness.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'AssertionGroups' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
{
_httpClientFactory = httpClientFactory;
}

public List<AssertionGroup> AssertionGroups { get; set; }

public async Task<List<Result>> RunAssertionsAsync()
{
var assertionResults = new List<Result>();

foreach (var group in AssertionGroups)
{
var tasks = new List<Task<Result>>();

foreach (var assertion in group.Assertions)
{
tasks.Add(RunAssertionAsync(assertion));
}

var results = await Task.WhenAll(tasks);

assertionResults.AddRange(results);
}

return assertionResults;
}

private async Task<Result> RunAssertionAsync(IAssertion assertion)
{
var result = new Result
{
AssertionName = assertion.Name,
IsSuccess = false,
ErrorMessage = string.Empty
};

var stopwatch = Stopwatch.StartNew();
try
{
using var client = _httpClientFactory.CreateClient();
client.Timeout = assertion.Timeout;

var request = new HttpRequestMessage(assertion.Method, assertion.Endpoint);
request.Content = assertion.RequestContent;

var response = await client.SendAsync(request);

if (response.StatusCode == assertion.ExpectedResponseCode)
{
if (assertion.ExpectedResponseContent != null)
{
var responseContent = await response.Content.ReadAsStringAsync();

if (responseContent == await assertion.ExpectedResponseContent.ReadAsStringAsync())
{
result.IsSuccess = true;
}
else
{
result.ErrorMessage = "Response content does not match expected content.";
}
}
else
{
result.IsSuccess = true;
}
}
else
{
result.ErrorMessage = $"Expected response code {assertion.ExpectedResponseCode}, but received {response.StatusCode}.";
}
}
catch (Exception ex)
{
result.ErrorMessage = ex.Message;
}
stopwatch.Stop();

result.ElapsedTime = stopwatch.Elapsed;

return result;
}
}
37 changes: 37 additions & 0 deletions Frank.Testing.ApiTesting/Assertion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Net;

namespace Frank.Testing.ApiTesting;

public class Assertion<TRequest, TResponse>
{
public string? Name { get; set; }

public Uri Endpoint { get; set; }

Check warning on line 9 in Frank.Testing.ApiTesting/Assertion.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Endpoint' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

public HttpMethod Method { get; set; }

Check warning on line 11 in Frank.Testing.ApiTesting/Assertion.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Method' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

public TimeSpan Timeout { get; set; }

public TRequest? Request { get; set; }

public TResponse? ExpectedResponse { get; set; }

public HttpStatusCode ExpectedResponseCode { get; set; }
}

public class Assertion : IAssertion
{
public string? Name { get; set; }

public Uri Endpoint { get; set; }

Check warning on line 26 in Frank.Testing.ApiTesting/Assertion.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Endpoint' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 26 in Frank.Testing.ApiTesting/Assertion.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Endpoint' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

public HttpMethod Method { get; set; }

Check warning on line 28 in Frank.Testing.ApiTesting/Assertion.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Method' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 28 in Frank.Testing.ApiTesting/Assertion.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Method' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

public TimeSpan Timeout { get; set; }

public HttpContent? RequestContent { get; set; }

public HttpContent? ExpectedResponseContent { get; set; }

public HttpStatusCode ExpectedResponseCode { get; set; }
}
7 changes: 7 additions & 0 deletions Frank.Testing.ApiTesting/AssertionGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Frank.Testing.ApiTesting;

public class AssertionGroup
{
public string GroupName { get; set; }

Check warning on line 5 in Frank.Testing.ApiTesting/AssertionGroup.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'GroupName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public List<IAssertion> Assertions { get; set; }

Check warning on line 6 in Frank.Testing.ApiTesting/AssertionGroup.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Assertions' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
}
18 changes: 18 additions & 0 deletions Frank.Testing.ApiTesting/AssertionPrecursor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Net;

namespace Frank.Testing.ApiTesting;

public class AssertionPrecursor<TResponse>
{
public string? Name { get; set; }

public Uri Endpoint { get; set; }

Check warning on line 9 in Frank.Testing.ApiTesting/AssertionPrecursor.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Endpoint' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 9 in Frank.Testing.ApiTesting/AssertionPrecursor.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Endpoint' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

public HttpMethod Method { get; set; }

Check warning on line 11 in Frank.Testing.ApiTesting/AssertionPrecursor.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Method' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 11 in Frank.Testing.ApiTesting/AssertionPrecursor.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'Method' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

public TimeSpan Timeout { get; set; }

public TResponse? ExpectedResponse { get; set; }

public HttpStatusCode ExpectedResponseCode { get; set; }
}
12 changes: 12 additions & 0 deletions Frank.Testing.ApiTesting/Frank.Testing.ApiTesting.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Test your api with this simple library</Description>
<PackageTags>api, test, rest, http, client</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup>

</Project>
14 changes: 14 additions & 0 deletions Frank.Testing.ApiTesting/IAssertion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Net;

namespace Frank.Testing.ApiTesting;

public interface IAssertion
{
string? Name { get; set; }
Uri Endpoint { get; set; }
HttpMethod Method { get; set; }
TimeSpan Timeout { get; set; }
HttpContent? RequestContent { get; set; }
HttpContent? ExpectedResponseContent { get; set; }
HttpStatusCode ExpectedResponseCode { get; set; }
}
8 changes: 8 additions & 0 deletions Frank.Testing.ApiTesting/IAssertionGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Frank.Testing.ApiTesting;

public interface IAssertionGroup
{
string GroupName { get; set; }

SortedList<uint, IAssertion> Assertions { get; set; }
}
11 changes: 11 additions & 0 deletions Frank.Testing.ApiTesting/Result.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Frank.Testing.ApiTesting;

public class Result
{
public string AssertionName { get; set; }

Check warning on line 5 in Frank.Testing.ApiTesting/Result.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'AssertionName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 5 in Frank.Testing.ApiTesting/Result.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'AssertionName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public bool IsSuccess { get; set; }
public string? ErrorMessage { get; set; }
public TimeSpan ElapsedTime { get; set; }

public HttpContent? ResponseContent { get; set; }
}
7 changes: 7 additions & 0 deletions Frank.Testing.ApiTesting/ResultGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Frank.Testing.ApiTesting;

public class ResultGroup
{
public string GroupName { get; set; }

Check warning on line 5 in Frank.Testing.ApiTesting/ResultGroup.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'GroupName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 5 in Frank.Testing.ApiTesting/ResultGroup.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'GroupName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public List<Result> AssertionResults { get; set; }

Check warning on line 6 in Frank.Testing.ApiTesting/ResultGroup.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'AssertionResults' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 6 in Frank.Testing.ApiTesting/ResultGroup.cs

View workflow job for this annotation

GitHub Actions / Release Job / Release Job

Non-nullable property 'AssertionResults' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
}
36 changes: 36 additions & 0 deletions Frank.Testing.ApiTesting/ResultsFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace Frank.Testing.ApiTesting;

using System.Collections.Generic;
using System.Xml.Linq;

public class ResultsFormatter
{
public string FormatResults(List<ResultGroup> groups)
{
var xDocument = new XDocument();
var root = new XElement("TestGroups");

foreach (var group in groups)
{
var groupElement = new XElement("Group",
new XAttribute("Name", group.GroupName));

foreach (var assertion in group.AssertionResults)
{
var testElement = new XElement("Test",
new XElement("Name", assertion.AssertionName),
new XElement("Success", assertion.IsSuccess),
new XElement("ErrorMessage", assertion.ErrorMessage ?? string.Empty),
new XElement("ElapsedTime", assertion.ElapsedTime.ToString("g"))
);

groupElement.Add(testElement);
}

root.Add(groupElement);
}

xDocument.Add(root);
return xDocument.ToString();
}
}
17 changes: 0 additions & 17 deletions Frank.Testing.Logging/DictionaryExtensions.cs

This file was deleted.

6 changes: 3 additions & 3 deletions Frank.Testing.Logging/Frank.Testing.Logging.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Frank.PulseFlow.Logging" Version="1.5.0" />
<PackageReference Include="Frank.Reflection" Version="1.0.0" />
<PackageReference Include="Frank.PulseFlow.Logging" Version="1.6.0" />
<PackageReference Include="Frank.Reflection" Version="1.1.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="xunit.extensibility.core" Version="2.6.4" />
<PackageReference Include="xunit.extensibility.core" Version="2.6.5" />
</ItemGroup>

<ItemGroup>
Expand Down
8 changes: 7 additions & 1 deletion Frank.Testing.Logging/LoggingBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ public static class LoggingBuilderExtensions
/// <returns>The modified ILoggingBuilder with the test logging added.</returns>
public static ILoggingBuilder AddPulseFlowTestLoggingProvider(this ILoggingBuilder builder, ITestOutputHelper outputHelper, LogLevel logLevel = LogLevel.Debug)
{
builder.ClearProviders();
builder.AddPulseFlow();
builder.Services.AddSingleton(outputHelper);
builder.Services.AddSingleton<IFlow, TestLoggingOutputFlow>();
builder.Services.Configure<TestLoggerSettings>(options =>
{
options.LogLevel = logLevel;
});
builder.Services.AddPulseFlow(flowBuilder => flowBuilder.AddFlow<TestLoggingOutputFlow>());
builder.Services.AddSingleton<ILoggerProvider>(provider => new TestLoggerProvider(provider.GetRequiredService<IConduit>()));
return builder;
}
}
9 changes: 6 additions & 3 deletions Frank.Testing.Logging/PulseFlowTestLogger.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
using Frank.PulseFlow;
using Frank.PulseFlow.Logging;
using Frank.Reflection;

using Microsoft.Extensions.Logging;

namespace Frank.Testing.Logging;

public class PulseFlowTestLogger(IConduit conduit) : ILogger
public class PulseFlowTestLogger(IConduit conduit, string categoryName) : ILogger
{
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
=> conduit.SendAsync(new LogPulse<TState>(logLevel, eventId, state, exception, formatter, "TestLogger")).GetAwaiter().GetResult();
=> conduit.SendAsync(new LogPulse(logLevel, eventId, exception, categoryName, formatter(state, exception))).GetAwaiter().GetResult();

public bool IsEnabled(LogLevel logLevel) => true;

public IDisposable? BeginScope<TState>(TState state) where TState : notnull => new PulseFlowLoggerScope<TState>(state);
}
}

public class PulseFlowTestLogger<T>(IConduit conduit) : PulseFlowTestLogger(conduit, typeof(T).GetDisplayName()), ILogger<T>;
4 changes: 4 additions & 0 deletions Frank.Testing.Logging/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ public static class ServiceCollectionExtensions
/// <returns>The modified IServiceCollection with the test logging added.</returns>
public static IServiceCollection AddTestLogging(this IServiceCollection services, ITestOutputHelper outputHelper, LogLevel logLevel = LogLevel.Debug)
{
services.Configure<TestLoggerSettings>(options =>
{
options.LogLevel = logLevel;
});
services.AddLogging(builder => builder.AddPulseFlowTestLoggingProvider(outputHelper, logLevel));
return services;
}
Expand Down
41 changes: 41 additions & 0 deletions Frank.Testing.Logging/SimpleTestLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Frank.Reflection;

using Microsoft.Extensions.Logging;

using Xunit.Abstractions;

namespace Frank.Testing.Logging;

public class SimpleTestLogger<T> : SimpleTestLogger, ILogger<T>
{
public SimpleTestLogger(ITestOutputHelper outputHelper, LogLevel logLevel) : base(outputHelper, logLevel, typeof(T).GetDisplayName())
{
}
}

public class SimpleTestLogger : ILogger
{
private readonly ITestOutputHelper _outputHelper;
private readonly LogLevel _logLevel;
private string _categoryName;

public SimpleTestLogger(ITestOutputHelper outputHelper, LogLevel logLevel, string categoryName)
{
_outputHelper = outputHelper;
_logLevel = logLevel;
_categoryName = categoryName;
}

public IDisposable? BeginScope<TState>(TState state) where TState : notnull
=> new TestLoggerScope<TState>(state);

public bool IsEnabled(LogLevel logLevel) => logLevel >= _logLevel;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (logLevel < _logLevel)
return;

_outputHelper.WriteLine($"[{logLevel}]: {formatter(state, exception)}");
}
}
Loading

0 comments on commit 42a7ce7

Please sign in to comment.