عنوان:

‫دیگر ILogger را Mock نکنید: تست لاگینگ در دات‌نت با FakeLogger


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۷/۲۴ ۰۹:۳۰
آدرس: www.dntips.ir
«ماک کردن» (Mocking) وابستگی‌ها در فرآیند توسعه نرم‌افزار، گاهی اوقات می‌تواند چالش‌برانگیز باشد؛ مخصوصاً زمانی که شما مالک آن اینترفیس نیستید و تعاملات با آن اینترفیس در کد، از طریق «متدهای توسعه» (Extension Methods) انجام می‌شود. این دقیقاً مشکلی است که در مورد ILogger از کتابخانه پایه‌ی لاگینگ مایکروسافت با آن روبرو هستیم.
اینترفیس ILogger نسبتاً ساده است، اما شما به ندرت به طور مستقیم با آن تعامل می‌کنید (مثلاً متد Log آن را مستقیماً فراخوانی نمی‌کنید، بلکه از LogInformation، LogError و... استفاده می‌کنید که همگی Extension Method هستند). پس چگونه می‌خواهید تست کنید که یک متد با پارامترهای درستی فراخوانی شده تا از صحت عملیات لاگینگ خود مطمئن شوید؟
می‌توان ILogger<> را با استفاده از کتابخانه‌هایی مانند Moq ماک کرد و آن را به SUT (System Under Test یا سیستم تحت تست) تزریق کرد و فراخوانی‌های آن را بررسی نمود. اما این کار می‌تواند درگیرکننده و پیچیده شود و همچنین شما مجبور می‌شوید کدهای بررسی ماک (Mock Verification) خود را بین پروژه‌های مختلف کپی کنید. این اصلاً ایده‌آل نیست.
اینجاست که FakeLogger وارد می‌شود!
FakeLogger به شما دسترسی کامل به تمام داده‌هایی را می‌دهد که در طول اجرای کد شما به ILogger ارسال شده‌اند و به شما اجازه می‌دهد تا در تست‌های خود، مقادیر آن‌ها را به سادگی Assert (بررسی) کنید.

راه‌اندازی و شروع به کار

این ابزار در ابتدا در پکیج‌های دیگری قرار داشت، اما اکنون می‌توانید آن را در پکیج NuGet زیر پیدا کنید:
  • Package:Microsoft.Extensions.Diagnostics.Testing
  • Namespace:Microsoft.Extensions.Logging.Testing
پس از اینکه این پکیج را به پروژه‌ی تست خود اضافه کردید، آماده‌اید تا از آن در یونیت تست‌های خود استفاده کنید.

استفاده پایه (Basic Usage)

برای نمایش این قابلیت، ما به یک SUT نمونه نیاز داریم. کلاسی به نام DemoService را در نظر بگیرید:
public class DemoService(ILogger<DemoService> logger)
{
    public void LogMe()
    {
        logger.LogInformation("This should be logged.");
    }
}
در سرویس بالا، ما می‌خواهیم تست کنیم که وقتی متد LogMe اجرا می‌شود، متد لاگ مناسب (در اینجا LogInformation) فراخوانی شده است.
ما با ایجاد یک نمونه از SUT خود شروع می‌کنیم و این کار نیازمند ساخت یک نمونه از FakeLogger است.
// Arrange
var fakeLogger = new FakeLogger<DemoService>();
var sut = new DemoService(fakeLogger);

// Act
sut.LogMe();

// Assert
fakeLogger.LatestRecord.Message.Should().Be("This should be logged.");
از آنجایی که FakeLogger<> اینترفیس ILogger<> را پیاده‌سازی می‌کند، می‌توانیم به سادگی نمونه‌ای از آن را به سازنده‌ی DemoService پاس دهیم.
هنگامی که سیستم در بخش Act تست ما (به عنوان بخشی از جریان تست‌نویسی AAA: Arrange, Act, Assert) اجرا شد، باید نتایج را Assert کنیم.
ساده‌ترین راه برای این کار، بررسی پراپرتی LatestRecord در fakeLogger است. این پراپرتی، آخرین فراخوانی به نمونه‌ی لاگر را در سیستم تحت تست افشا می‌کند.

بررسی سطح لاگ (Log Level)

FakeLogger فقط پیام را ضبط نمی‌کند، بلکه تمام جزئیات لاگ را ثبت می‌کند. شما به راحتی می‌توانید سطح لاگ (Log Level) را نیز بررسی کنید:
// Assert (ادامه تست قبلی)
fakeLogger.LatestRecord.Message.Should().Be("This should be logged.");
fakeLogger.LatestRecord.LogLevel.Should().Be(LogLevel.Information);
این کار تضمین می‌کند که نه تنها پیام درست لاگ شده، بلکه سطح اهمیت آن (Info, Warning, Error) نیز مطابق انتظار بوده است.

تست لاگ‌های دارای Scope

FakeLogger تنها برای پیام‌های ساده نیست؛ قدرت واقعی آن زمانی مشخص می‌شود که بخواهیم Scope ها را تست کنیم.

چرا از Scope استفاده کنیم؟

یک Scope لاگ در دات‌نت، راهی برای پیوست کردن اطلاعات زمینه‌ای (Contextual Information) به تمام رکوردهای لاگ در یک بلوک کد فراهم می‌کند. به آن به عنوان یک مجموعه‌ای سبک از جفت‌ کلید-مقدارهایی (Key-Value) فکر کنید که پیام‌های لاگ را غنی می‌کند، بدون اینکه نیاز باشد همان فیلدها را در هر خط لاگ تکرار کنید.
برای مثال، شما ممکن است یک RequestID، CorrelationID یا شناسه یک موجودیت (Entity Identifier) را به یک Scope اضافه کنید. این کار باعث می‌شود هر لاگی که در آن بلوک کد ثبت می‌شود، به طور خودکار آن متادیتای زمینه‌ای را به همراه داشته باشد. این قابلیت، فیلتر کردن و درک لاگ‌ها را در سیستم‌های توزیع‌شده یا هنگام دیباگ کردن یک درخواست خاص، بسیار آسان‌تر می‌کند.

استفاده از FakeLogger با Scopes

بیایید متد خود را طوری تغییر دهیم که از Scope استفاده کند:
public partial class DemoService(ILogger<DemoService> logger)
{
    public void LogMe(Guid id)
    {
        // ایجاد یک اسکوپ با اطلاعات زمینه‌ای
        using var _ = logger.BeginScope(new Dictionary<string, object> { { "id", id } });

        logger.LogInformation("This should be logged.");
    }
}
در اینجا، فراخوانی BeginScope یک زمینه (Context) لاگ با یک کلید واحد (id) ایجاد می‌کند. هنگامی که logger.LogInformation اجرا می‌شود, Scope به طور خودکار آن داده‌های زمینه‌ای را ضمیمه می‌کند. اما در یک یونیت تست، چگونه می‌توانیم تأیید کنیم که Scope واقعاً اعمال شده است؟

تست کردن استفاده از Scope با FakeLogger

FakeLogger تمام رکوردهای لاگ، از جمله تمپلیت‌های پیام، سطوح لاگ، استثناها و Scopeها را ردیابی می‌کند. بیایید تست کنیم که DemoService ما به درستی پیام را لاگ کرده و id را به Scope پیوست کرده است:
public class DemoServiceTests
{
    [Fact]
    public void ScopeUsage()
    {
        // Arrange
        var fakeLogger = new FakeLogger<DemoService>();
        var sut = new DemoService(fakeLogger);
        var id = Guid.NewGuid();

        // Act
        sut.LogMe(id);

        // Assert
        // 1. بررسی پیام لاگ
        fakeLogger.LatestRecord.Message.Should().Be("This should be logged.");

        // 2. بررسی اسکوپ
        // Scopes مجموعه‌ای از اسکوپ‌های فعال در زمان لاگ است
        var scope = fakeLogger.LatestRecord.Scopes[0] as Dictionary<string, object>;
        
        scope.Should().NotBeNull();
        scope.Keys.Should().Contain("id");
        scope.Values.Should().Contain(id);
    }
}
در این تست:
  • ما SUT را با استفاده از FakeLogger می‌سازیم (بدون نیاز به ماک یا Setup).
  • متد LogMe فراخوانی می‌شود و یک رکورد لاگ تولید می‌کند.
  • ما محتوای پیام را Assert می‌کنیم و سپس کالکشن Scopes ثبت شده را بازرسی می‌کنیم.
  • هر Scope به عنوان یک object ضبط می‌شود، و در این مورد، ما می‌دانیم که آن یک Dictionary است که حاوی id ماست.
این رویکرد تمیز، خوانا و توسط خود فریم‌ورک پشتیبانی می‌شود و نیاز به Wrapperهای سفارشی برای لاگر یا تنظیمات شکننده‌ی تأیید ماک (Mock Verification) را از بین می‌برد.

نکته (منفی؟): تنها نکته‌ی منفی این است که شما احتمالاً باید از جزئیات پیاده‌سازی نحوه‌ی اعمال Scope مطلع باشید (مثلاً در اینجا باید می‌دانستید که به صورت Dictionary پاس داده شده است). اما برای این نوع تست‌های سطح پایین، این میزان از آگاهی قابل قبول است.

نتیجه‌گیری

تست کردن لاگینگ به طور تاریخی کاری ناخوشایند بوده‌است؛ به خصوص زمانی که نیاز به تأیید Scope یا داده‌های ساختاریافته (Structured Data) داشتیم. با FakeLogger مایکروسافت، اکنون به سادگی می‌توان Assert کرد که پیام‌های لاگ، سطوح و Scopeها همگی به درستی اعمال شده‌اند، بدون اینکه به کتابخانه‌های ماکینگ شخص ثالث یا Helperهای تست سفارشی متوسل شویم.

در این مقاله، ما بررسی کردیم:
  • چگونه FakeLogger جایگزین ماک کردن ILogger می‌شود.
  • نحوه‌ی راه‌اندازی و استفاده‌ی پایه از آن برای بررسی پیام و سطح لاگ.
  • چگونه Scopeهای لاگ، متادیتای زمینه‌ای مفیدی را ارائه می‌دهند.
  • چگونه FakeLogger می‌تواند آن Scopeها را برای تست، ضبط و افشا کند.

با پذیرش FakeLogger، می‌توانید بر روی تست منطق برنامه و رفتار لاگینگ خود به طور همزمان تمرکز کنید؛ به صورت شفاف، ساده و بدون کدهای تکراری و اضافی (Boilerplate).