Unit-Test mit ILogger in ASP.NET Core

127

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 können, habe ich 2 Abhängigkeiten, a IDAOund aILogger

Und dies ist meine Testklasse. Ich benutze xUnit zum Testen und Moq zum Erstellen von Mock und Stub. Ich kann DAOeinfach verspotten , aber mit dem ILoggerich nicht weiß, was ich tun soll, übergebe ich einfach null und kommentiere den Aufruf zum Anmelden des Controllers aus beim Test ausführen. Gibt es eine Möglichkeit zu testen, aber den Logger 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());
    }
}
duc
quelle
1
Sie können einen Mock als Stub verwenden, wie Ilya vorschlägt, wenn Sie nicht tatsächlich testen möchten, ob die Protokollierungsmethode selbst aufgerufen wurde. Wenn dies der Fall ist, funktioniert das Verspotten des Loggers nicht und Sie können verschiedene Ansätze ausprobieren. Ich habe einen kurzen Artikel geschrieben , der verschiedene Ansätze zeigt. Der Artikel enthält ein vollständiges GitHub-Repo mit den verschiedenen Optionen . Am Ende ist meine Empfehlung, Ihren eigenen Adapter zu verwenden, anstatt direkt mit dem ILogger <T> -Typ zu arbeiten, wenn Sie in der Lage sein müssen
Schmied
Wie @ssmith erwähnt hat, gibt es einige Probleme bei der Überprüfung der tatsächlichen Anrufe für ILogger. Er hat einige gute Vorschläge in seinem Blogpost und ich bin mit meiner Lösung gekommen, die die meisten Probleme in der Antwort unten zu lösen scheint .
Ilya Chernomordik

Antworten:

138

Verspotten Sie es einfach und jede andere Abhängigkeit:

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.AbstractionsPaket installieren , um es verwenden zu können ILogger<T>.

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>();
Ilya Chumakov
quelle
5
Um sich im Debug-Ausgabefenster anzumelden, rufen Sie ab Werk AddDebug () auf: var factory = serviceProvider.GetService <ILoggerFactory> () .AddDebug ();
Spottedmahn
3
Ich fand den "Real Logger" -Ansatz effektiver!
DanielV
1
Der Real Logger-Teil eignet sich auch hervorragend zum Testen von LogConfiguration und LogLevel in bestimmten Szenarien.
Martin Lottering
Dieser Ansatz ermöglicht nur das Stub, nicht jedoch die Überprüfung von Anrufen. Ich bin mit meiner Lösung gekommen, die die meisten Probleme mit der Überprüfung in der folgenden Antwort zu lösen scheint .
Ilya Chernomordik
100

Eigentlich habe ich gefunden, Microsoft.Extensions.Logging.Abstractions.NullLogger<>was wie eine perfekte Lösung aussieht. Installieren Sie das Paket Microsoft.Extensions.Logging.Abstractionsund folgen Sie dem Beispiel, um es zu konfigurieren und zu verwenden:

using Microsoft.Extensions.Logging;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddSingleton<ILoggerFactory, NullLoggerFactory>();

    ...
}
using Microsoft.Extensions.Logging;

public class MyClass : IMyClass
{
    public const string ErrorMessageILoggerFactoryIsNull = "ILoggerFactory is null";

    private readonly ILogger<MyClass> logger;

    public MyClass(ILoggerFactory loggerFactory)
    {
        if (null == loggerFactory)
        {
            throw new ArgumentNullException(ErrorMessageILoggerFactoryIsNull, (Exception)null);
        }

        this.logger = loggerFactory.CreateLogger<MyClass>();
    }
}

und Unit Test

//using Microsoft.VisualStudio.TestTools.UnitTesting;
//using Microsoft.Extensions.Logging;

[TestMethod]
public void SampleTest()
{
    ILoggerFactory doesntDoMuch = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
    IMyClass testItem = new MyClass(doesntDoMuch);
    Assert.IsNotNull(testItem);
}   
Amir Shitrit
quelle
Dies scheint nur für .NET Core 2.0 zu funktionieren, nicht für .NET Core 1.1.
Thorkil Værge
3
@adospace, Ihr Kommentar ist viel nützlicher als die Antwort
Johnny 5
Können Sie ein Beispiel geben, wie dies funktionieren würde? Beim Unit-Test möchte ich, dass Protokolle im Ausgabefenster angezeigt werden. Ich bin mir nicht sicher, ob dies der Fall ist.
J86
@adospace Soll das in startup.cs gehen?
Raklos
1
@raklos hum, nein, es soll in einer
Startmethode
31

Verwenden Sie einen benutzerdefinierten Logger, der ITestOutputHelper(von xunit) zum Erfassen von Ausgaben und Protokollen verwendet. Das Folgende ist ein kleines Beispiel, das nur das statein 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 in Ihren Unittests wie

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
  }
}
Jehof
quelle
1
Hallo. Das funktioniert gut für mich. Jetzt, wie ich meine Protokollinformationen überprüfen oder anzeigen kann
Malik Saifullah
Ich führe die Unit-Testfälle direkt von VS aus. Ich habe keine Konsole dafür
Malik Saifullah
1
@ Maliksaifullah Ich benutze Resharper. Lassen Sie mich das mit vs überprüfen
Jehof
1
@maliksaifullah Der TestExplorer von VS bietet einen Link zum Öffnen der Ausgabe eines Tests. Wählen Sie Ihren Test im TestExplorer aus und unten befindet sich ein Link
Jehof
1
Das ist großartig, danke! Ein paar Vorschläge: 1) Dies muss nicht generisch sein, da der Typparameter nicht verwendet wird. Durch die Implementierung von Just ILoggerwird es breiter nutzbar. 2) Der BeginScopesollte sich nicht selbst zurückgeben, da dies bedeutet, dass alle getesteten Methoden, die einen Bereich während des Laufs beginnen und beenden, den Logger entsorgen. Erstellen Sie stattdessen eine private verschachtelte "Dummy" -Klasse, die IDisposableeine Instanz davon implementiert und zurückgibt (und dann IDisposableaus entfernt XunitLogger).
Tobias J
26

Für .net Core 3 Antworten, die Moq verwenden

Zum Glück stakx eine schöne bereitgestellt Abhilfe . Also poste ich es in der Hoffnung, dass es Zeit für andere sparen kann (es hat eine Weile gedauert, um die Dinge herauszufinden):

 loggerMock.Verify(
                x => x.Log(
                    LogLevel.Information,
                    It.IsAny<EventId>(),
                    It.Is<It.IsAnyType>((o, t) => string.Equals("Index page say hello", o.ToString(), StringComparison.InvariantCultureIgnoreCase)),
                    It.IsAny<Exception>(),
                    (Func<It.IsAnyType, Exception, string>) It.IsAny<object>()),
                Times.Once);
Ivan Samygin
quelle
Du hast meinen Tag gerettet. Danke.
KiddoDeveloper
15

Wenn ich meine 2 Cent addiere, ist dies eine Hilfserweiterungsmethode, die normalerweise in eine statische Hilfsklasse eingefügt 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 verwenden Sie es wie folgt:

//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 leicht erweitern, um jede Erwartung zu verspotten (z. B. Erwartung, Botschaft usw.).

Mahmoud Hanafy
quelle
Dies ist eine sehr elegante Lösung.
MichaelDotKnox
Ich stimme zu, diese Antwort war sehr gut. Ich verstehe nicht, warum es nicht so viele Stimmen hat
Farzad
1
Fab. Hier ist eine Version für Nicht-Generika ILogger: gist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb
Tim Abell
Wäre es möglich, ein Mock zu erstellen, um die in LogWarning übergebene Zeichenfolge zu überprüfen? Zum Beispiel:It.Is<string>(s => s.Equals("A parameter is empty!"))
Serhat
Das hilft sehr. Das einzige fehlende Teil für mich ist, wie ich einen Rückruf für das Modell einrichten kann, das in die XUnit-Ausgabe schreibt. Trifft nie den Rückruf für mich.
Flipdoubt
6

Es ist einfach, wie andere Antworten vorschlagen, Mock zu übergeben ILogger, aber es wird plötzlich viel problematischer zu überprüfen, ob tatsächlich Anrufe an den Logger getätigt wurden. Der Grund ist, dass die meisten Anrufe nicht zur ILoggerSchnittstelle selbst gehören.

Die meisten Aufrufe sind also Erweiterungsmethoden, die die einzige LogMethode der Schnittstelle aufrufen . Der Grund dafür ist, dass die Implementierung der Schnittstelle viel einfacher ist, wenn Sie nur eine und nicht viele Überladungen haben, die auf dieselbe Methode hinauslaufen.

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

Hier ist ein Beispiel für eine Methode, mit der ich gearbeitet habe NSubstitute:

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 hätten Sie die Methode direkt verwendet. Der Trick dabei ist, dass unsere Erweiterungsmethode Priorität erhält, da sie in Namespaces "näher" ist als die ursprüngliche, sodass sie stattdessen verwendet wird.

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

PS Für Moq kann man den Ansatz des Schreibens eine Erweiterungsmethode für die Verwendung , Mock<ILogger<T>>das tut Verifyähnliche Ergebnisse zu erzielen.

PPS Dies funktioniert in .Net Core 3 nicht mehr. Weitere Informationen finden Sie in diesem Thread: https://github.com/nsubstitute/NSubstitute/issues/597#issuecomment-573742574

Ilya Chernomordik
quelle
Warum überprüfen Sie Logger-Anrufe? Sie sind kein Teil der Geschäftslogik. Wenn etwas Schlimmes passiert ist, möchte ich lieber das tatsächliche Programmverhalten überprüfen (z. B. einen Fehlerbehandler aufrufen oder eine Ausnahme auslösen), als eine Nachricht zu protokollieren.
Ilya Chumakov
1
Nun, ich denke, es ist ziemlich wichtig, das auch zu testen, zumindest in einigen Fällen. Ich habe zu oft gesehen, dass ein Programm stillschweigend fehlschlägt, daher halte ich es für sinnvoll, zu überprüfen, ob die Protokollierung stattgefunden hat, als eine Ausnahme aufgetreten ist, z. B. Und es ist nicht wie "entweder oder", sondern es wird sowohl das tatsächliche Programmverhalten als auch die Protokollierung getestet.
Ilya Chernomordik
5

Bereits erwähnt können Sie es wie jede andere Schnittstelle verspotten.

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

So weit, ist es gut.

Das Schöne ist, dass Sie Moqdamit überprüfen können , ob bestimmte Anrufe ausgeführt wurden . Zum Beispiel überprüfe ich hier, ob das Protokoll mit einem bestimmten aufgerufen wurde Exception.

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>>()));

Wenn Sie Verifyden Punkt verwenden, tun Sie dies gegen die reale LogMethode von der ILoogerSchnittstelle und nicht gegen die Erweiterungsmethoden.

Guillem
quelle
4

Aufbauend auf der Arbeit von @ ivan-samygin und @stakx finden Sie hier Erweiterungsmethoden, die auch mit der Ausnahme und allen Protokollwerten (KeyValuePairs) übereinstimmen können.

Diese funktionieren (auf meinem Computer;)) mit .Net Core 3, Moq 4.13.0 und Microsoft.Extensions.Logging.Abstractions 3.1.0.

/// <summary>
/// Verifies that a Log call has been made, with the given LogLevel, Message and optional KeyValuePairs.
/// </summary>
/// <typeparam name="T">Type of the class for the logger.</typeparam>
/// <param name="loggerMock">The mocked logger class.</param>
/// <param name="expectedLogLevel">The LogLevel to verify.</param>
/// <param name="expectedMessage">The Message to verify.</param>
/// <param name="expectedValues">Zero or more KeyValuePairs to verify.</param>
public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, LogLevel expectedLogLevel, string expectedMessage, params KeyValuePair<string, object>[] expectedValues)
{
    loggerMock.Verify(mock => mock.Log(
        expectedLogLevel,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((o, t) => MatchesLogValues(o, expectedMessage, expectedValues)),
        It.IsAny<Exception>(),
        It.IsAny<Func<object, Exception, string>>()
        )
    );
}

/// <summary>
/// Verifies that a Log call has been made, with LogLevel.Error, Message, given Exception and optional KeyValuePairs.
/// </summary>
/// <typeparam name="T">Type of the class for the logger.</typeparam>
/// <param name="loggerMock">The mocked logger class.</param>
/// <param name="expectedMessage">The Message to verify.</param>
/// <param name="expectedException">The Exception to verify.</param>
/// <param name="expectedValues">Zero or more KeyValuePairs to verify.</param>
public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, string expectedMessage, Exception expectedException, params KeyValuePair<string, object>[] expectedValues)
{
    loggerMock.Verify(logger => logger.Log(
        LogLevel.Error,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((o, t) => MatchesLogValues(o, expectedMessage, expectedValues)),
        It.Is<Exception>(e => e == expectedException),
        It.Is<Func<It.IsAnyType, Exception, string>>((o, t) => true)
    ));
}

private static bool MatchesLogValues(object state, string expectedMessage, params KeyValuePair<string, object>[] expectedValues)
{
    const string messageKeyName = "{OriginalFormat}";

    var loggedValues = (IReadOnlyList<KeyValuePair<string, object>>)state;

    return loggedValues.Any(loggedValue => loggedValue.Key == messageKeyName && loggedValue.Value.ToString() == expectedMessage) &&
           expectedValues.All(expectedValue => loggedValues.Any(loggedValue => loggedValue.Key == expectedValue.Key && loggedValue.Value == expectedValue.Value));
}
Mathias R.
quelle
1

Das bloße Erstellen eines Dummys ILoggerist für Unit-Tests nicht sehr wertvoll. Sie sollten auch überprüfen, ob die Protokollierungsaufrufe getätigt wurden. Sie können ein Mock injizieren ILoggermit Moq aber den Anruf Überprüfung kann ein wenig schwierig sein. Dieser Artikel befasst sich ausführlich mit der Überprüfung mit Moq.

Hier ist ein sehr einfaches Beispiel aus dem Artikel:

_loggerMock.Verify(l => l.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), Times.Exactly(1));

Es wird überprüft, ob eine Informationsnachricht protokolliert wurde. Wenn wir jedoch komplexere Informationen über die Nachricht wie die Nachrichtenvorlage und die genannten Eigenschaften überprüfen möchten, wird es schwieriger:

_loggerMock.Verify
(
    l => l.Log
    (
        //Check the severity level
        LogLevel.Error,
        //This may or may not be relevant to your scenario
        It.IsAny<EventId>(),
        //This is the magical Moq code that exposes internal log processing from the extension methods
        It.Is<It.IsAnyType>((state, t) =>
            //This confirms that the correct log message was sent to the logger. {OriginalFormat} should match the value passed to the logger
            //Note: messages should be retrieved from a service that will probably store the strings in a resource file
            CheckValue(state, LogTest.ErrorMessage, "{OriginalFormat}") &&
            //This confirms that an argument with a key of "recordId" was sent with the correct value
            //In Application Insights, this will turn up in Custom Dimensions
            CheckValue(state, recordId, nameof(recordId))
    ),
    //Confirm the exception type
    It.IsAny<NotImplementedException>(),
    //Accept any valid Func here. The Func is specified by the extension methods
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
    //Make sure the message was logged the correct number of times
    Times.Exactly(1)
);

Ich bin mir sicher, dass Sie dasselbe mit anderen spöttischen Frameworks tun können, aber die ILoggerBenutzeroberfläche stellt sicher, dass es schwierig ist.

Christian Findlay
quelle
1
Ich stimme dem Gefühl zu, und wie Sie sagen, kann es etwas schwierig werden, den Ausdruck aufzubauen. Ich hatte oft das gleiche Problem, also habe ich kürzlich Moq.Contrib.ExpressionBuilders.Logging zusammengestellt, um eine flüssige Oberfläche bereitzustellen, die es viel schmackhafter macht.
rgvlee
1

Ist ein noch aktueller. Einfache Protokollierung zur Ausgabe in Tests für .net Core> = 3

[Fact]
public void SomeTest()
{
    using var logFactory = LoggerFactory.Create(builder => builder.AddConsole());
    var logger = logFactory.CreateLogger<AccountController>();
    
    var controller = new SomeController(logger);

    var result = controller.SomeActionAsync(new Dto{ ... }).GetAwaiter().GetResult();
}
photoscar
quelle
0

Verwenden Sie Telerik Just Mock , um eine verspottete Instanz des Loggers zu erstellen:

using Telerik.JustMock;
...
context = new XDbContext(Mock.Create<ILogger<XDbContext>>());
Bigpony
quelle
0

Ich habe versucht, diese Logger-Schnittstelle mit NSubstitute zu verspotten (und bin fehlgeschlagen, weil Arg.Any<T>()ein Typparameter erforderlich ist, den ich nicht bereitstellen kann), habe jedoch einen Testlogger (ähnlich der Antwort von @ jehof) wie folgt erstellt:

    internal sealed class TestLogger<T> : ILogger<T>, IDisposable
    {
        private readonly List<LoggedMessage> _messages = new List<LoggedMessage>();

        public IReadOnlyList<LoggedMessage> Messages => _messages;

        public void Dispose()
        {
        }

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

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

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            var message = formatter(state, exception);
            _messages.Add(new LoggedMessage(logLevel, eventId, exception, message));
        }

        public sealed class LoggedMessage
        {
            public LogLevel LogLevel { get; }
            public EventId EventId { get; }
            public Exception Exception { get; }
            public string Message { get; }

            public LoggedMessage(LogLevel logLevel, EventId eventId, Exception exception, string message)
            {
                LogLevel = logLevel;
                EventId = eventId;
                Exception = exception;
                Message = message;
            }
        }
    }

Sie können einfach auf alle protokollierten Nachrichten zugreifen und alle damit verbundenen wichtigen Parameter bestätigen.

Igor Kustov
quelle