ASP.NET Core'da ILogger ile birim testi nasıl yapılır


129

Bu benim denetleyicim:

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

Gördüğünüz gibi 2 bağımlılığım var, a IDAOve aILogger

Ve bu benim test sınıfım, test etmek için xUnit ve sahte ve saplama oluşturmak için Moq kullanıyorum, DAOkolay alay edebilirim , ancak ILoggerne yapacağımı bilmiyorum, bu yüzden sadece null geçirip denetleyiciye giriş yapma çağrısını yorumluyorum test çalıştırıldığında. Test etmenin ama yine de kaydediciyi bir şekilde tutmanın bir yolu var mı?

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

1
Eğer gerçekten günlüğe kaydetme yönteminin çağrıldığını test etmeye çalışmıyorsanız, Ilya'nın önerdiği gibi, bir taklidi saplama olarak kullanabilirsiniz. Durum buysa, kaydediciyle alay etmek işe yaramaz ve birkaç farklı yaklaşım deneyebilirsiniz. Çeşitli yaklaşımları gösteren kısa bir makale yazdım . Makale , farklı seçeneklerin her biri ile eksiksiz bir GitHub deposu içerir . Sonunda, önerim, doğrudan ILogger <T> tipiyle çalışmak yerine kendi adaptörünüzü kullanmanızdır, eğer bunu yapabilmeniz gerekiyorsa
29'da ssmith

@Ssmith'in bahsettiği gibi, gerçek çağrıları doğrulamakta bazı sorunlar var ILogger. Blog gönderisinde bazı iyi önerileri var ve aşağıdaki cevaptaki sorunların çoğunu çözecek gibi görünen çözümümle geldim .
Ilya Chernomordik

Yanıtlar:


142

Sadece onunla ve diğer bağımlılıklarla alay edin:

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

Muhtemelen Microsoft.Extensions.Logging.Abstractionskullanmak için paket yüklemeniz gerekecek ILogger<T>.

Üstelik gerçek bir kaydedici oluşturabilirsiniz:

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

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

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

5
hata ayıklama çıktı penceresine giriş yapmak için fabrikada AddDebug () çağırın: var factory = serviceProvider.GetService <ILoggerFactory> () .AddDebug ();
Spottedmahn

3
"Gerçek kaydedici" yaklaşımını daha etkili buldum!
DanielV

1
Gerçek kaydedici kısmı ayrıca belirli senaryolarda LogConfiguration ve LogLevel'i test etmek için harika çalışır.
Martin Lottering

Bu yaklaşım yalnızca saplamaya izin verir, aramaların doğrulanmasına izin vermez. Sorunların çoğunu aşağıdaki cevapta doğrulama ile çözen çözümümle geldim .
Ilya Chernomordik

102

Aslında, Microsoft.Extensions.Logging.Abstractions.NullLogger<>hangisinin mükemmel bir çözüm gibi göründüğünü buldum . Paketi kurun Microsoft.Extensions.Logging.Abstractions, ardından yapılandırmak ve kullanmak için örneği izleyin:

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

ve birim testi

//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);
}   

Bu, .NET Core 1.1 için değil, yalnızca .NET Core 2.0 için çalışıyor gibi görünüyor.
Thorkil Værge

3
@adospace, yorumunuz cevaptan çok daha yararlı
johnny 5

Bunun nasıl çalışacağına dair bir örnek verebilir misiniz? Birim testi yaparken, günlüklerin çıktı penceresinde görünmesini istiyorum, bunu yapıp yapmadığından emin değilim.
J86

@adospace Bunun startup.cs'ye gitmesi mi gerekiyor?
raklos

1
hum @raklos, hayır o ServiceCollection örneği test içindeki bir başlangıç yönteminde kullanılacak gerekiyordu
adospace

32

ITestOutputHelperÇıktı ve günlükleri yakalamak için (xunit'ten) kullanan özel bir kaydedici kullanın . Aşağıdaki, yalnızca stateçıktıya yazan küçük bir örnektir .

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()
    {
    }
}

Ünite testlerinizde aşağıdaki gibi kullanın

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
  }
}

1
Selam. bu benim için iyi çalışıyor. şimdi günlük bilgilerimi nasıl kontrol edebilir veya görüntüleyebilirim
malik saifullah

birim test durumlarını doğrudan VS'den çalıştırıyorum. bunun için konsolum yok
malik saifullah

1
@maliksaifullah resharper kullanıyorum. bunu vs ile kontrol
edeyim

1
@maliksaifullah VS'nin TestExplorer'ı, bir testin çıktısını açmak için bir bağlantı sağlar. TestExplorer'da testinizi seçin ve altta bir bağlantı var
Jehof

1
Bu harika, teşekkürler! Birkaç öneri: 1) tür parametresi kullanılmadığından bunun genel olması gerekmez. Uygulama ILogger, onu daha geniş bir şekilde kullanılabilir hale getirecektir. 2) BeginScope, çalışma sırasında bir kapsamı başlatan ve sona erdiren test edilen herhangi bir yöntem günlükçüyü elden çıkaracağından, kendi kendine dönmemelidir. Bunun yerine, bunun IDisposablebir örneğini uygulayan ve geri döndüren özel bir "kukla" iç içe sınıf oluşturun (daha sonra IDisposableburadan kaldırın XunitLogger).
Tobias J

27

Moq kullanan .net çekirdek 3 cevapları için

Neyse ki stakx güzel bir çözüm sağladı . Bu yüzden başkalarına zaman kazandırması umuduyla yayınlıyorum (olayları çözmek biraz zaman aldı):

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

Günümü kurtardın .. teşekkür ederim.
KiddoDeveloper

15

2 sentimi ekleyerek, Bu genellikle statik bir yardımcı sınıfa yerleştirilen bir yardımcı genişletme yöntemidir:

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

Sonra, bunu şu şekilde kullanırsın:

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

//Act

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

Ve elbette, herhangi bir beklentiyle (ör. Beklenti, mesaj, vb.)


Bu çok zarif bir çözüm.
MichaelDotKnox

Katılıyorum, bu cevap çok iyiydi. Neden bu kadar çok oyu olmadığını anlamıyorum
Ferzad

1
Fab. İşte jenerik olmayanlar için bir sürüm ILogger: gist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb
Tim Abell

LogWarning'de geçtiğimiz dizeyi kontrol etmek için sahte oluşturmak mümkün olabilir mi? Örneğin:It.Is<string>(s => s.Equals("A parameter is empty!"))
Serhat

Bu çok yardımcı oluyor. Benim için eksik olan tek şey, XUnit çıktısına yazan sahte üzerinde bir geri aramayı nasıl kurabilirim? Benim için asla geri aranmaz.
flipdoubt

6

Diğer cevapların sahte geçmeyi önerdiği için kolay ILogger, ancak çağrıların gerçekten kaydediciye yapıldığını doğrulamak aniden çok daha sorunlu hale geliyor. Bunun nedeni, çoğu aramanın aslında ILoggerarayüzün kendisine ait olmamasıdır .

Bu nedenle, çağrıların çoğu Log, arabirimin tek yöntemini çağıran uzantı yöntemleridir . Görünüşe göre, sadece bir tane varsa ve aynı yönteme indirgenen çok fazla aşırı yüklenmeye sahip değilseniz, arayüzün uygulanmasını yapmanın çok daha kolay olmasıdır.

Bunun dezavantajı elbette, doğrulamanız gereken arama yaptığınız aramadan çok farklı olduğu için bir aramanın yapıldığını doğrulamanın aniden çok daha zor olmasıdır. Bunu aşmak için bazı farklı yaklaşımlar var ve alay çerçevesi için özel uzantı yöntemlerinin yazmayı en kolay hale getireceğini buldum.

İşte çalışmak için yaptığım bir yönteme bir örnek 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>>());
    }

}

Ve şu şekilde kullanılabilir:

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

Yöntemi doğrudan kullanmışsınız gibi görünür, buradaki hile, uzantı yöntemimizin öncelik kazanmasıdır çünkü ad alanlarında orijinal olandan "daha yakın" olduğundan, onun yerine kullanılacaktır.

Ne yazık ki istediğimizi% 100 vermiyor, yani hata mesajları o kadar iyi olmayacak, çünkü doğrudan bir dizgeyi değil, dizgiyi içeren bir lambda'yı kontrol ediyoruz, ancak% 95 hiç yoktan iyidir :) Ayrıca bu yaklaşım test kodunu yapacak

PS için Moq tek bir uzantı yöntemi yazma yaklaşımı kullanabilirsiniz Mock<ILogger<T>>yaptığı Verifybenzer sonuçlar elde etmek.

PPS Bu artık .Net Core 3'te çalışmıyor, daha fazla ayrıntı için bu konuyu kontrol edin: https://github.com/nsubstitute/NSubstitute/issues/597#issuecomment-573742574


Kaydedici aramalarını neden doğruluyorsunuz? İş mantığının bir parçası değildirler. Kötü bir şey olursa, bir mesajı günlüğe kaydetmektense gerçek program davranışını doğrulamayı (bir hata işleyiciyi çağırmak veya bir istisna atmak gibi) tercih ederim.
Ilya Chumakov

1
En azından bazı durumlarda bunu test etmenin de oldukça önemli olduğunu düşünüyorum. Bir programın sessizce başarısız olduğunu pek çok kez gördüm, bu nedenle, örneğin bir istisna meydana geldiğinde günlüğe kaydetmenin gerçekleştiğini doğrulamanın mantıklı olduğunu düşünüyorum ve bu "ya da" gibi değil, hem gerçek program davranışını hem de günlüğe kaydetmeyi test ediyor.
Ilya Chernomordik

5

Zaten bahsettiğim gibi başka bir arayüz gibi alay edebilirsiniz.

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

Çok uzak çok iyi.

Güzel şey kullanabilirsiniz olmasıdır Moqiçin belirli çağrılar yapılmıştır doğrulamak . Örneğin burada, günlüğün belirli biriyle çağrılıp çağrılmadığını kontrol ediyorum 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>>()));

VerifyNoktayı kullanırken , uzantı yöntemlerine değil, arayüzden gerçek Logyönteme karşı yapmaktır ILooger.


5

@ İvan-samygin ve @stakx'in çalışmalarını daha da geliştirerek, burada İstisna ve tüm günlük değerleri (KeyValuePairs) ile de eşleşebilecek uzantı yöntemleri var.

Bunlar (benim makinemde;) .Net Core 3, Moq 4.13.0 ve Microsoft.Extensions.Logging.Abstractions 3.1.0 ile çalışır.

/// <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));
}


1

Sadece bir kukla oluşturmak ILoggerbirim testi için çok değerli değildir. Ayrıca kayıt aramalarının yapıldığını da doğrulamalısınız. MoqILogger ile bir taklit enjekte edebilirsiniz , ancak aramayı doğrulamak biraz zor olabilir. Bu makale , Moq ile doğrulama hakkında derinlemesine bilgi verir.

İşte makaleden çok basit bir örnek:

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

Bir bilgi mesajının kaydedildiğini doğrular. Ancak, mesajla ilgili mesaj şablonu ve adlandırılmış özellikler gibi daha karmaşık bilgileri doğrulamak istersek, daha karmaşık hale gelir:

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

Eminim diğer alaycı çerçevelerle de aynısını yapabilirsiniz, ancak ILoggerarayüz bunun zor olmasını sağlar.


1
Duyguya katılıyorum ve dediğiniz gibi ifadeyi oluşturmak biraz zor olabilir. Aynı sorunu sık sık yaşadım, bu yüzden yakın zamanda Moq.Contrib.ExpressionBuilders.Logging'i bir araya getirerek, onu çok daha lezzetli kılan akıcı bir arayüz sağladı.
rgvlee

1

Hala gerçekse. .Net core> = 3 testlerinde çıktı almak için basit bir yol

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


0

Bu Logger arayüzünü NSubstitute kullanarak taklit etmeye çalıştım (ve sağlayamadığım Arg.Any<T>()bir tür parametresi gerektirdiği için başarısız oldum), ancak aşağıdaki şekilde bir test günlükleyici (@ jehof'un cevabına benzer şekilde) oluşturdum:

    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;
            }
        }
    }

Kaydedilen tüm mesajlara kolayca erişebilir ve onunla sağlanan tüm anlamlı parametreleri onaylayabilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.