To polskie tłumaczenie poprzedniego artykułu (rozszerzone!).
log4net jest frameworkiem służącym do logowania. Poznałem go dzięki setkom świetnych opinii w Internecie. Sposób użycia jest bardzo prosty, a zarazem niesamowicie elastyczny – po prostu logujemy wszystko za pomocą interfejsu ILog (dalej nazywam je po prostu „loggerami”, pewnie nie do końca po polsku :-)), a następnie konfigurujemy w XML-u (lub programistycznie), gdzie zarejestrowane logi mają trafiać.
Interfejs ILog możemy pobrać na użytek danej klasy na dwa sposoby:
- Wykorzystując typ (w tym przypadku naszej klasy, w której znajduje się logger, oczywiście może być to dowolny typ):
namespace Foo.Test
{
public class MyTestClass
{
Ilog log = LogManager.GetLogger(typeof(MyTestClass));
}
}
- po prostu poprzez string:
namespace Foo.Test
{
public class MyTestClass
{
Ilog log = LogManager.GetLogger("Mój obiekt logowania");
}
}
Jednak zalecam tę pierwszą metodę. Dlaczego? Bardzo ułatwia nam to hierarchiczną organizację logerów. Dodatkowo, w pierwszym przypadku w naszych logach od razu pojawi się nazwa klasy wraz z przestrzenią nazw, czyli będziemy od razu widzieli „Foo.Test.MyTestClass”. Potencjalna refaktoryzacja (jak np. zmiana nazwy klasy, czy przestrzeni nazw) nie spowoduje, że będziemy musieli zmieniać nazwy naszych loggerów.
Niedawno, przeglądając kod projektu Subtext (tak, to robię po godzinach ;-)) wpadłem na fajny trick służący do pobierania loggerów. Co, gdybyśmy nie musieli jawnie podawać typu przy pobieraniu loggera, tylko pewna logika sama by znalazłaby typ klasy?
Może ten test rozjaśni, o co dokładnie chodzi:
namespace Test
{
using log4net;
using log4net.Config;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
[TestFixture]
public class LoggingTests
{
[Test]
public void TestLoggerName()
{
BasicConfigurator.Configure();
ILog log = Logging.GetLog();
Assert.That(log.Logger.Name, Is.EqualTo("Test.LoggingTests"));
}
}
}
Chcielibyśmy dostać obiekt ILog bez konieczności podawania jawnie typu klasy. W tym przypadku chcemy uzyskać „Test.LoggingTests”, bo tak nazywa się klasa testu (klasa, która potrzebuje loggera).
Oczywiście ten test się nie skompiluje bez klasy Logging. Definiujemy ją z metodą GetLog, na razie zwracającą null:
public static class Logging
{
public static ILog GetLog()
{
return null;
}
}
Udało nam się skompilować, oczywiście test zwraca błąd (tak jak powinien, jeżeli chcemy stosować najlepsze praktyki testów jednostkowych)!
Spróbujmy zaimplementować naszą klasę pomocniczą. Niech GetLog użyje metody GetCallerType do pobrania typu klasy, do której chcemy pobrać loggera. Zauważmy, że nigdzie nie podajemy co to za klasa.
public static ILog GetLog()
{
return LogManager.GetLogger(GetCallerType());
}
Zwróci to loggera o typie, który pobierze metoda:
[MethodImpl(MethodImplOptions.NoInlining)]
private static Type GetCallerType()
{
return new StackFrame(2, false).GetMethod().DeclaringType;
}
Ten malutki kawałek kodu pobiera ramkę ze stosu cofając się o dwie wywołania metod. Dla naszego testu, dostaniemy metodę TestLoggerName. Stąd już niedaleko, do pobrania typu klasy zawierającej tę metodę. Sprytne!
Test powinien przejść. Tutaj pełen kod naszej klasy pomocniczej:
using System;
using log4net;
using System.Runtime.CompilerServices;
using System.Diagnostics;
namespace Logging
{
/// <summary>
/// A static class to fetch the logger.
/// </summary>
public static class Logging
{
/// <summary>
/// This replaces the LogManager.GetLogger(typeof(CurrentClass)).
/// You only need to declare the log in your class like this:
/// protected ILog log = Logging.GetLog();
/// </summary>
/// <returns></returns>
public static ILog GetLog()
{
return LogManager.GetLogger(GetCallerType());
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static Type GetCallerType()
{
return new StackFrame(2, false).GetMethod().DeclaringType;
}
}
}
Tags: .net, log4net, pl, unit testing
Nice. Można jednak zrobić to jeszcze fajniej poprzez metody rozszerzające:
# namespace Logging
# {
# public static class LogExtensions
# {
# public static ILog GetLog(this Object obj)
# {
# return LogManager.GetLogger(obj.GetType());
# }
# }
# }
Teraz kod po przetłumaczeniu:
# public void TestLoggerName()
# {
# BasicConfigurator.Configure();
# ILog log = GetLog();
#
# Assert.That(log.Logger.Name, Is.EqualTo(„Test.LoggingTests”));
# }
[...] w komentarzu do poprzedniej notki o log4net przedstawił inny, niewątpliwie sprytny sposób automatyzacji pobierania nazwanego loggera za [...]