it-swarm.com.de

Gewusst wie: Komponententest mit ILogger in ASP.NET Core

Das ist mein Controller:

public class BlogController : Controller
{
    private IDAO<Blog> _blogDAO;
    private readonly ILogger<BlogController> _logger;

    public BlogController(ILogger<BlogController> logger, IDAO<Blog> blogDAO)
    {
        this._blogDAO = blogDAO;
        this._logger = logger;
    }
    public IActionResult Index()
    {
        var blogs = this._blogDAO.GetMany();
        this._logger.LogInformation("Index page say hello", new object[0]);
        return View(blogs);
    }
}

Wie Sie sehen, habe ich 2 Abhängigkeiten, eine IDAO und eine ILogger

Und dies ist meine Testklasse. Ich benutze xUnit zum Testen und Moq, um Mock und Stub zu erstellen. Ich kann DAO einfach simulieren, aber mit der ILogger weiß ich nicht, was ich tun soll im Controller beim Testlauf. Gibt es eine Möglichkeit, den Logger zu testen und trotzdem irgendwie zu behalten?

public class BlogControllerTest
{
    [Fact]
    public void Index_ReturnAViewResult_WithAListOfBlog()
    {
        var mockRepo = new Mock<IDAO<Blog>>();
        mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
        var controller = new BlogController(null,mockRepo.Object);

        var result = controller.Index();

        var viewResult = Assert.IsType<ViewResult>(result);
        var model = Assert.IsAssignableFrom<IEnumerable<Blog>>(viewResult.ViewData.Model);
        Assert.Equal(2, model.Count());
    }
}
47
duc

Einfach nur so wie alle anderen Abhängigkeiten:

var mock = new Mock<ILogger<BlogController>>();
ILogger<BlogController> logger = mock.Object;

//or use this short equivalent 
logger = Mock.Of<ILogger<BlogController>>()

var controller = new BlogController(logger);

Sie müssen wahrscheinlich das Microsoft.Extensions.Logging.Abstractions-Paket installieren, um ILogger<T> verwenden zu können. 

Außerdem können Sie einen echten Logger erstellen:

var serviceProvider = new ServiceCollection()
    .AddLogging()
    .BuildServiceProvider();

var factory = serviceProvider.GetService<ILoggerFactory>();

var logger = factory.CreateLogger<BlogController>();
62
Ilya Chumakov

Eigentlich habe ich Microsoft.Extensions.Logging.Abstractions.NullLogger<> gefunden, der wie eine perfekte Lösung aussieht.

47
Amir Shitrit

Verwenden Sie einen benutzerdefinierten Logger, der ITestOutputHelper (von xunit) zum Erfassen von Ausgabe und Protokollen verwendet. Das folgende ist ein kleines Beispiel, das nur die Variable state in die Ausgabe schreibt.

public class XunitLogger<T> : ILogger<T>, IDisposable
{
    private ITestOutputHelper _output;

    public XunitLogger(ITestOutputHelper output)
    {
        _output = output;
    }
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        _output.WriteLine(state.ToString());
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    public IDisposable BeginScope<TState>(TState state)
    {
        return this;
    }

    public void Dispose()
    {
    }
}

Verwenden Sie es gerne in Ihren Tests

public class BlogControllerTest
{
  private XunitLogger<BlogController> _logger;

  public BlogControllerTest(ITestOutputHelper output){
    _logger = new XunitLogger<BlogController>(output);
  }

  [Fact]
  public void Index_ReturnAViewResult_WithAListOfBlog()
  {
    var mockRepo = new Mock<IDAO<Blog>>();
    mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
    var controller = new BlogController(_logger,mockRepo.Object);
    // rest
  }
}
9
Jehof

Dies ist eine Hilfserweiterungsmethode, die normalerweise in eine statische Hilfsklasse eingeordnet wird:

static class MockHelper
{
    public static ISetup<ILogger<T>> MockLog<T>(this Mock<ILogger<T>> logger, LogLevel level)
    {
        return logger.Setup(x => x.Log(level, It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()));
    }

    private static Expression<Action<ILogger<T>>> Verify<T>(LogLevel level)
    {
        return x => x.Log(level, 0, It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>());
    }

    public static void Verify<T>(this Mock<ILogger<T>> mock, LogLevel level, Times times)
    {
        mock.Verify(Verify<T>(level), times);
    }
}

Dann benutzt du es so:

//Arrange
var logger = new Mock<ILogger<YourClass>>();
logger.MockLog(LogLevel.Warning)

//Act

//Assert
logger.Verify(LogLevel.Warning, Times.Once());

Und natürlich können Sie es problemlos erweitern, um alle Erwartungen (d. H. Erwartungen, Nachrichten usw.) zu verspotten.

3
Mahmoud Hanafy

Wie bereits erwähnt, können Sie es wie jede andere Schnittstelle nachahmen.

var logger = new Mock<ILogger<QueuedHostedService>>();

So weit, ist es gut.

Eine schöne Sache ist, dass Sie mit Moqüberprüfen können, ob bestimmte Anrufe ausgeführt wurden. Zum Beispiel überprüfe ich hier, ob das Protokoll mit einer bestimmten Exception aufgerufen wurde.

logger.Verify(m => m.Log(It.Is<LogLevel>(l => l == LogLevel.Information), 0,
            It.IsAny<object>(), It.IsAny<TaskCanceledException>(), It.IsAny<Func<object, Exception, string>>()));

Bei der Verwendung von Verify ist es wichtig, dies gegen die echte Log-Methode der ILooger-Schnittstelle und nicht gegen die Erweiterungsmethoden auszuführen.

1
guillem

Wie andere Antworten nahelegen, ist es leicht, die Scheinvariable ILogger zu übergeben, aber es wird plötzlich viel problematischer, zu überprüfen, ob Anrufe tatsächlich an den Logger gerichtet wurden. Der Grund ist, dass die meisten Aufrufe nicht zur ILogger-Schnittstelle gehören.

Die meisten Aufrufe sind daher Erweiterungsmethoden, die die einzige Log-Methode der Schnittstelle aufrufen. Der Grund ist, dass es scheinbar einfacher ist, die Implementierung der Schnittstelle vorzunehmen, wenn Sie nur eine und nicht viele Überlastungen haben, die auf dieselbe Methode zurückzuführen sind.

Der Nachteil ist natürlich, dass es plötzlich viel schwieriger ist, zu überprüfen, ob ein Anruf getätigt wurde, da der Anruf, den Sie überprüfen sollten, sich sehr von dem Anruf unterscheidet, den Sie getätigt haben. Es gibt einige unterschiedliche Ansätze, um dies zu umgehen, und ich habe festgestellt, dass benutzerdefinierte Erweiterungsmethoden für das Spott-Framework das Schreiben am einfachsten machen.

Hier ist ein Beispiel einer Methode, die ich für die Arbeit mit NSubstitute erstellt habe:

public static class LoggerTestingExtensions
{
    public static void LogError(this ILogger logger, string message)
    {
        logger.Log(
            LogLevel.Error,
            0,
            Arg.Is<FormattedLogValues>(v => v.ToString() == message),
            Arg.Any<Exception>(),
            Arg.Any<Func<object, Exception, string>>());
    }

}

Und so kann es verwendet werden:

_logger.Received(1).LogError("Something bad happened");   

Es sieht genau so aus, als ob Sie die Methode direkt verwendet hätten. Der Trick hier ist, dass unsere Erweiterungsmethode Priorität erhält, weil sie in Namespaces "näher" ist als die ursprüngliche, daher wird sie stattdessen verwendet.

Es gibt leider nicht 100% das, was wir wollen, nämlich Fehlermeldungen werden nicht so gut sein, da wir nicht direkt auf einer Zeichenfolge prüfen, sondern auf einem Lambda, das die Zeichenfolge beinhaltet, aber 95% sind besser als nichts :) Zusätzlich Dieser Ansatz wird den Testcode erstellen 

P.S. Für Moq kann man den Ansatz verwenden, eine Erweiterungsmethode für den Mock<ILogger<T>> zu schreiben, die Verify verwendet, um ähnliche Ergebnisse zu erzielen.

0

Und wenn Sie StructureMap/Lamar verwenden:

var c = new Container(_ =>
{
    _.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
});

Dokumente:

0