Implementing a custom log provider in tests

As we already have seen, the logging system of ASP.NET Core is designed for maximum extensibility. In this section, we will learn how to implement a custom logging provider that we can use in our tests. All the test classes that are present in the Catalog.API.Tests project use InMemoryApplicationFactory<T> to run a web server and provide HttpClient to call the API. As you may have noticed, the tests don't return an explicit error when one of the tests fails. For example, let's examine the following test method in the  ItemControllerTests class:

public class ItemController : IClassFixture<InMemoryApplicationFactory<Startup>>
{
...

[Fact]
public async Task update_should_returns_not_found
_when_item_is_not_present
()
{
var client = _factory.CreateClient();

var httpContent = new StringContent(jsonPayload.ToString(),
Encoding.UTF8, "application/json");
var response = await client.PutAsync($"/api/items/
{
Guid.NewGuid()}", httpContent);

response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
}
..
}

If for any reason, the call returns an error, we will receive the following message on the test side:

...
Shouldly.ShouldAssertException : response.StatusCode
should be
HttpStatusCode.NotFound
but was
HttpStatusCode.InternalServerError
...

Here, we don't know why the API has returned an InternalServerError. Here, we can use the ITestOutputHelper interface provided by Xunit to create a new logger provider and use it in our tests. To declare a logger in ASP.NET Core, we need the following structure and components:

The preceding schema describes two main components: the TestOutputLoggerProvider type and the TestOutputLogger type. The TestOutputLoggerProvider type's purpose is to manage a list of logger instances. The TestOutputLogger class describes the actual implementation of the logging mechanism. Let's start by defining the custom ILogger component:

using System;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;

namespace Catalog.Fixtures
{
public class TestOutputLogger : ILogger
{
private readonly ITestOutputHelper _output;

public TestOutputLogger(ITestOutputHelper output) =>
_output = output;

public IDisposable BeginScope<TState>(TState state) => null;

public bool IsEnabled(LogLevel logLevel) =>
logLevel == LogLevel.Error;

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

_output.WriteLine($"{logLevel.ToString()} -
{
exception.Message} - {exception.StackTrace}");

}
}
}

ITestOutputClass implements the methods provided by the ILogger interface. First of all, it declares an ITestOutputHelper field in the constructor. Then, it uses the _output attribute inside the concrete implementation of the Log method by calling the _output.WriteLine method. The class also implements the IsEnabled method to check whether the log level corresponds to the LogLevel.Error field. If the log record doesn't do this, LogLevel.Error is written in the console's output. To finalize this implementation, we need a logger provider to initialize the custom logger. Let's continue by creating another class called TestOutputLoggerProvider, which extends the ILoggerProvider interface:

using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Catalog.Fixtures
{
public class TestOutputLoggerProvider : ILoggerProvider
{
private readonly ConcurrentDictionary<string, TestOutputLogger>
_loggers = new ConcurrentDictionary
<string, TestOutputLogger>();

private readonly ITestOutputHelper _testOutputHelper;

public TestOutputLoggerProvider(ITestOutputHelper
testOutputHelper) => _testOutputHelper = testOutputHelper;

public ILogger CreateLogger(string categoryName) =>
_loggers.GetOrAdd(categoryName, name => new
TestOutputLogger(_testOutputHelper));

public void Dispose() => _loggers.Clear();
}
}

TestOutputLoggerProvider defines a ConcurrentDictionary, which contains a pair of string and TestOutputLogger; it also accepts the ITestOutputHelper interface as an interface, which is used in the CreateLogger method to add the logger to the logging pipeline. By doing this, we can integrate the custom logger into our tests. We will use the InMemoryApplicationFactory<T> class, which is implemented in the Catalog.Fixtures project, to add the custom logger, as follows:

using Xunit.Abstractions;
...

namespace
Catalog.Fixtures
{
public class InMemoryApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
private ITestOutputHelper _testOutputHelper;
public void SetTestOutputHelper(ITestOutputHelper
testOutputHelper)

{
_testOutputHelper = testOutputHelper;
}
protected override void ConfigureWebHost(IWebHostBuilder
builder)
{
builder
.UseEnvironment("Testing")
.ConfigureTestServices(services =>
{
...
if (_testOutputHelper != null)
{
services.AddLogging(cfg => cfg.AddProvider(new
TestOutputLoggerProvider(_testOutputHelper)));

}
...
});
}
}
}

The preceding class declares a new ITestOutputHelper attribute type and defines a setter. It is possible to add our custom logger by calling the AddProvider extension method inside the ConfigureTestService class by creating a new instance of TestOutputLoggerProvider. After making these changes, we can proceed by integrating the custom logger into the ItemControllerTests class:

...
namespace
Catalog.API.Tests.Controllers
{
public class ItemControllerTests :
IClassFixture
<InMemoryApplicationFactory<Startup>>
{
private readonly InMemoryApplicationFactory<Startup> _factory;
public ItemControllerTests(InMemoryApplicationFactory<Startup>
factory, ITestOutputHelper outputHelper)

{
_factory = factory;
_factory.SetTestOutputHelper(outputHelper);
}
...

As you can see, ItemControllerTests calls _factory.SetTestOutputHelper in the constructor by setting the injected ITestOutputHelper interface. Now, we'll get detailed error messages every time a test throws an error. It is essential to note that the ITestOutputHelper interface is assigned in the test class, which means this is the only point where it is possible to obtain the interface using dependency injection. In the next section, we will learn how to implement health checks related to web service dependencies.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.72.78