Cách kiểm tra đơn vị với ILogger trong ASP.NET Core


128

Đây là bộ điều khiển của tôi:

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

Như bạn thấy tôi có 2 phụ thuộc, a IDAOvà aILogger

Và đây là lớp thử nghiệm của tôi, tôi sử dụng xUnit để kiểm tra và Moq để tạo ra giả và sơ khai, tôi có thể giả DAOdễ dàng, nhưng với ILoggertôi không biết phải làm gì nên tôi chỉ chuyển null và nhận xét cuộc gọi để đăng nhập bộ điều khiển khi chạy thử. Có cách nào để kiểm tra nhưng vẫn giữ logger bằng cách nào đó?

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
Bạn có thể sử dụng một bản giả như một sơ khai, như Ilya gợi ý, nếu bạn không thực sự cố gắng kiểm tra rằng chính phương thức ghi nhật ký đã được gọi. Nếu đó là trường hợp, nhạo báng logger không hoạt động, và bạn có thể thử một vài cách tiếp cận khác nhau. Tôi đã viết một bài báo ngắn cho thấy một loạt các phương pháp tiếp cận. Bài viết bao gồm một repo GitHub đầy đủ với mỗi tùy chọn khác nhau . Cuối cùng, khuyến nghị của tôi là sử dụng bộ điều hợp của riêng bạn thay vì làm việc trực tiếp với loại <T> ILogger, nếu bạn cần có thể
ssmith

Như @ssmith đã đề cập, có một số rắc rối với việc xác minh các cuộc gọi thực tế ILogger. Anh ấy có một số gợi ý tốt trong blogpost của mình và tôi đã đưa ra giải pháp của mình mà dường như giải quyết được hầu hết các rắc rối trong câu trả lời dưới đây .
Ilya Chernomordik

Câu trả lời:


139

Chỉ cần chế giễu nó cũng như bất kỳ sự phụ thuộc nào khác:

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

Bạn có thể sẽ cần phải cài đặt Microsoft.Extensions.Logging.Abstractionsgói để sử dụng ILogger<T>.

Hơn nữa, bạn có thể tạo một logger thực sự:

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

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

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

5
để đăng nhập vào cửa sổ gỡ lỗi, hãy gọi AddDebug () trên nhà máy: var Factory = serviceProvider.GetService <ILoggerFactory> (). AddDebug ();
spốmmahn

3
Tôi thấy cách tiếp cận "logger thực" hiệu quả hơn!
DanielV

1
Phần logger thực cũng hoạt động tuyệt vời để kiểm tra LogConfiguration và LogLevel trong các tình huống cụ thể.
Martin Xổ số 23/11/18

Cách tiếp cận này sẽ chỉ cho phép sơ khai, nhưng không xác minh các cuộc gọi. Tôi đã đi kèm với giải pháp của mình dường như giải quyết hầu hết các rắc rối với xác minh trong câu trả lời dưới đây .
Ilya Chernomordik

100

Trên thực tế, tôi đã tìm thấy Microsoft.Extensions.Logging.Abstractions.NullLogger<>nó trông giống như một giải pháp hoàn hảo. Cài đặt gói Microsoft.Extensions.Logging.Abstractions, sau đó làm theo ví dụ để cấu hình và sử dụng nó:

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

và kiểm tra đơn vị

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

Điều này dường như chỉ hoạt động với .NET Core 2.0, không phải .NET Core 1.1.
Thorkil Værge

3
@adospace, nhận xét của bạn hữu ích hơn nhiều so với câu trả lời
johnny 5

Bạn có thể đưa ra một ví dụ về cách thức này sẽ làm việc? Khi kiểm tra đơn vị, tôi muốn các bản ghi xuất hiện trong cửa sổ đầu ra, tôi không chắc liệu điều này có làm điều đó không.
J86

@adospace Đây có phải là đi trong startup.cs?
raklos

1
@raklos hum, không nên sử dụng phương thức khởi động trong thử nghiệm trong đó ServiceCollection được khởi tạo
adospace

31

Sử dụng một logger tùy chỉnh sử dụng ITestOutputHelper(từ xunit) để nắm bắt đầu ra và nhật ký. Sau đây là một mẫu nhỏ chỉ ghi stateđầu ra.

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

Sử dụng nó trong unittests của bạn như

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
Chào. công việc này tốt cho tôi bây giờ làm thế nào tôi có thể kiểm tra hoặc xem thông tin nhật ký của mình
malik saifullah

tôi đang chạy các trường hợp thử nghiệm đơn vị trực tiếp từ VS. tôi không có bàn điều khiển cho điều đó
malik saifullah 8/12/18

1
@maliksaifullah im sử dụng chia sẻ lại. hãy để tôi kiểm tra xem với vs
Jehof

1
@maliksaifullah TestExplorer của VS cung cấp một liên kết để mở đầu ra của một bài kiểm tra. chọn bài kiểm tra của bạn trong TestExplorer và ở phía dưới có một liên kết
Jehof

1
Điều này thật tuyệt, cảm ơn! Một vài gợi ý: 1) điều này không cần phải chung chung, vì tham số loại không được sử dụng. Thực hiện chỉ ILoggersẽ làm cho nó có thể sử dụng rộng rãi hơn. 2) Không BeginScopenên trả về chính nó, vì điều đó có nghĩa là bất kỳ phương thức được thử nghiệm nào bắt đầu và kết thúc một phạm vi trong quá trình chạy sẽ loại bỏ logger. Thay vào đó, hãy tạo một lớp lồng "giả" riêng để thực hiện IDisposablevà trả về một thể hiện của nó (sau đó xóa IDisposablekhỏi XunitLogger).
Tobias J

27

Đối với .net core 3 câu trả lời đang sử dụng Moq

May mắn stakx cung cấp một cách giải quyết tốt đẹp . Vì vậy, tôi đang đăng nó với hy vọng nó có thể tiết kiệm thời gian cho người khác (phải mất một thời gian để tìm ra những điều này):

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

Bạn đã cứu ngày của tôi..Cảm ơn bạn.
Nhà phát triển Kiddo

15

Thêm 2 xu của tôi, Đây là phương thức mở rộng của trình trợ giúp thường được đặt trong lớp trình trợ giúp tĩnh:

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

Sau đó, bạn sử dụng nó như thế này:

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

//Act

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

Và tất nhiên bạn có thể dễ dàng mở rộng nó để chế nhạo bất kỳ kỳ vọng nào (ví dụ như thông báo, tin nhắn, v.v.


Đây là một giải pháp rất thanh lịch.
MichaelDotKnox

Tôi đồng ý, câu trả lời này là rất tốt. Tôi không hiểu tại sao nó không có nhiều phiếu bầu
Farzad

1
Fab. Đây là phiên bản dành cho người không chung chung ILogger: gist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb
Tim Abell

Có thể tạo giả để kiểm tra chuỗi chúng tôi đã chuyển trong LogWarning không? Ví dụ:It.Is<string>(s => s.Equals("A parameter is empty!"))
Serhat

Điều này giúp rất nhiều. Phần còn thiếu đối với tôi là làm thế nào tôi có thể thiết lập một cuộc gọi lại trên bản giả ghi vào đầu ra XUnit? Không bao giờ nhấn cuộc gọi lại cho tôi.
flipdoubt

6

Thật dễ dàng khi các câu trả lời khác đề nghị vượt qua giả ILogger, nhưng đột nhiên việc xác minh rằng các cuộc gọi thực sự được thực hiện để ghi nhật ký trở nên khó khăn hơn nhiều. Lý do là hầu hết các cuộc gọi không thực sự thuộc về ILoggergiao diện.

Vì vậy, hầu hết các cuộc gọi là các phương thức mở rộng gọi Logphương thức duy nhất của giao diện. Lý do có vẻ là việc thực hiện giao diện dễ dàng hơn nếu bạn chỉ có một và không có quá nhiều tình trạng quá tải theo cùng một phương pháp.

Hạn chế là tất nhiên là khó khăn hơn nhiều để xác minh rằng một cuộc gọi đã được thực hiện do cuộc gọi bạn nên xác minh rất khác với cuộc gọi mà bạn đã thực hiện. Có một số cách tiếp cận khác nhau để giải quyết vấn đề này và tôi đã thấy rằng các phương thức mở rộng tùy chỉnh cho khung mô phỏng sẽ giúp bạn dễ viết nhất.

Đây là một ví dụ về một phương pháp mà tôi đã thực hiện để làm việc với 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>>());
    }

}

Và đây là cách nó có thể được sử dụng:

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

Trông giống hệt như bạn đã sử dụng phương thức trực tiếp, mẹo ở đây là phương thức tiện ích mở rộng của chúng tôi được ưu tiên vì nó "gần" hơn trong không gian tên so với phương thức ban đầu, vì vậy nó sẽ được sử dụng thay thế.

Thật không may, nó không cung cấp 100% những gì chúng ta muốn, cụ thể là các thông báo lỗi sẽ không tốt như vậy, vì chúng ta không kiểm tra trực tiếp trên chuỗi mà thay vào đó là lambda liên quan đến chuỗi, nhưng 95% vẫn tốt hơn không có gì :) Ngoài ra Cách tiếp cận này sẽ làm cho mã kiểm tra

PS Đối với Moq, người ta có thể sử dụng phương pháp viết một phương thức mở rộng cho phương pháp Mock<ILogger<T>>đó Verifyđể đạt được kết quả tương tự.

PPS Điều này không hoạt động trong .Net Core 3 nữa, hãy kiểm tra chủ đề này để biết thêm chi tiết: https://github.com/nsubstolarship/NSubstolarship/issues/597#issuecomment-573742574


Tại sao bạn xác minh các cuộc gọi logger? Họ không phải là một phần của logic kinh doanh. Nếu có điều gì đó xấu xảy ra, tôi muốn xác minh hành vi chương trình thực tế (chẳng hạn như gọi trình xử lý lỗi hoặc ném ngoại lệ) thay vì ghi nhật ký tin nhắn.
Ilya Chumakov

1
Chà tôi nghĩ cũng khá quan trọng để kiểm tra điều đó, ít nhất là trong một số trường hợp. Tôi đã thấy quá nhiều lần rằng một chương trình thất bại trong âm thầm, vì vậy tôi nghĩ rằng việc xác minh rằng việc ghi nhật ký đã xảy ra khi có ngoại lệ xảy ra, ví dụ như nó không giống như "hoặc hoặc", mà là kiểm tra cả hành vi và ghi nhật ký chương trình thực tế.
Ilya Chernomordik

5

Đã đề cập bạn có thể giả định nó như bất kỳ giao diện khác.

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

Càng xa càng tốt.

Điều tuyệt vời là bạn có thể sử dụng Moqđể xác minh rằng các cuộc gọi nhất định đã được thực hiện . Ví dụ ở đây tôi kiểm tra rằng nhật ký đã được gọi với một cụ thể 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>>()));

Khi sử dụng Verifyđiểm là làm điều đó với Logphương thức thực từ ILoogergiao diện chứ không phải phương thức mở rộng.


5

Xây dựng hơn nữa về công việc của @ ivan-samygin và @stakx, đây là các phương thức mở rộng cũng có thể phù hợp với Ngoại lệ và tất cả các giá trị nhật ký (KeyValuePairs).

Các công việc này (trên máy của tôi;)) với .Net Core 3, Moq 4.13.0 và Microsoft.Extensions.Logging.Abstrilities 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));
}


1

Chỉ tạo ra một hình nộm ILoggerkhông có giá trị cho thử nghiệm đơn vị. Bạn cũng nên xác minh rằng các cuộc gọi đăng nhập đã được thực hiện. Bạn có thể tiêm giả ILoggervới Moq nhưng việc xác minh cuộc gọi có thể hơi khó. Bài viết này đi sâu về việc xác minh với Moq.

Đây là một ví dụ rất đơn giản từ bài viết:

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

Nó xác minh rằng một thông tin đã được ghi lại. Nhưng, nếu chúng tôi muốn xác minh thông tin phức tạp hơn về thông báo như mẫu tin nhắn và các thuộc tính được đặt tên, thì sẽ khó khăn hơn:

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

Tôi chắc chắn rằng bạn có thể làm tương tự với các khung mô phỏng khác, nhưng ILoggergiao diện đảm bảo rằng điều đó thật khó khăn.


1
Tôi đồng ý với tình cảm, và như bạn nói, nó có thể hơi khó khăn khi xây dựng biểu thức. Tôi đã có cùng một vấn đề, vì vậy, gần đây đã kết hợp Moq.Contrib.ExpressionBuilders.Logging để cung cấp một giao diện trôi chảy khiến nó trở nên ngon miệng hơn rất nhiều.
rgvlee

1

Nếu một thực tế vẫn còn. Cách đơn giản để đăng nhập vào đầu ra trong các thử nghiệm cho lõi .net> = 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();
}

0

Sử dụng Telerik Just Mock để tạo một phiên bản giả của logger:

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

0

Tôi đã cố gắng giả định giao diện Logger bằng NSubstolarship (và không thành công vì Arg.Any<T>()yêu cầu tham số loại mà tôi không thể cung cấp), nhưng cuối cùng đã tạo ra một trình ghi nhật ký thử nghiệm (tương tự như câu trả lời của @ jehof) theo cách sau:

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

Bạn có thể dễ dàng truy cập tất cả các thông điệp đã ghi lại và khẳng định tất cả các tham số có ý nghĩa được cung cấp cùng với nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.