Lese log4net-loggen i enhetstester


tirsdag 1. juni 2010 Testing C#

Av og til har jeg behov for å sjekke hva som logges til log4net i enhets- eller integrasjonstester. Dette er ikke "best practise", man bør normalt kunne verifisere forventet adferd på andre måter, men det finnes alltid unntak. I vår test-suite på rundt 700 tester har dette behoved dukket opp en håndfull steder.

Til dette formålet har jeg kommet opp med klassen nedenfor. C#-koden konfigurerer opp log4net med en appender som legger alt som logges til en List of LoggingEvent. For enkelhets skyld, siden jeg allerede normalt har en avhengighet til mockingrammeverket Moq, bruker jeg Moq til å opprette en appender for meg. Om du ikke bruker noe tilsvarende kan du selvsagt lage din egen in-memory appender.

    1 using System;
    2 using System.Collections.Generic;
    3 using Moq;
    4 using log4net.Appender;
    5 using log4net.Core;
    6 using log4net.Config;
    7 
    8 namespace SharedTestLib
    9 {
   10     /// <summary>
   11     /// Will capture all events logged through log4net 
   12     /// for investigation in automated tests
   13     /// </summary>
   14     public static class TestLog
   15     {
   16         private static bool _initialized;
   17         private static List<LoggingEvent> _log_events = new List<LoggingEvent>();
   18 
   19         /// <summary>
   20         /// FakeLog will start to capture log events. 
   21         /// Call it in TestFixtureSetUp.
   22         /// </summary>
   23         public static void Initialize()
   24         {
   25             if (_initialized)
   26                 return;
   27 
   28             var mockAppender = new Mock<IAppender>();
   29             mockAppender
   30                 .Setup(appender => appender.DoAppend(It.IsAny<LoggingEvent>()))
   31                 .Callback((LoggingEvent le) => _log_events.Add(le));
   32             BasicConfigurator.Configure(mockAppender.Object);
   33             _initialized = true;
   34         }
   35 
   36         /// <summary>
   37         /// Clears the events.
   38         /// Call it in SetUp.
   39         /// </summary>
   40         public static void Clear()
   41         {
   42             _log_events = new List<LoggingEvent>();
   43         }
   44 
   45         public static IEnumerable<LoggingEvent> Events
   46         {
   47             get
   48             {
   49                 return _log_events;
   50             }
   51         }
   52     }
   53 }

Det er ikke ofte jeg lager statiske klasser, men i dette tilfellet var det helt greit, og den blir veldig enkel å bruke i enhetstestene. Jeg valgte å implementere en Initialize()-metode for eksplisit initialisering, men en statisk konstruktør kunne nok gjort samme nytten. Initialize kan forøvrig (som du ser) kalles flere ganger, appenderen blir bare lagt til én gang.

Jeg er forresten klar over at log4net også har en MemoryAppender jeg kunne ha brukt, men min fremgangsmåte virket noe enklere sammenlignet med det jeg fant om den på nettet.

Videre kan du inkludere noen properties for å hente ut siste event av en bestemt type – det er ikke usannsynlig at det er akkurat det du er ute etter:

   55 #region Convenience methods
   56 
   57 public static LoggingEvent LastEvent { get { return _log_events.Last(); } }
   58 
   59 public static LoggingEvent LastDebug { get { return Last(Level.Debug); } }
   60 public static LoggingEvent LastInfo { get { return Last(Level.Info); } }
   61 public static LoggingEvent LastWarn { get { return Last(Level.Warn); } }
   62 public static LoggingEvent LastError { get { return Last(Level.Error); } }
   63 public static LoggingEvent LastFatal { get { return Last(Level.Fatal); } }
   64 
   65 private static LoggingEvent Last(Level level)
   66 {
   67     return _log_events.Where(e => e.Level == level).Last();
   68 }
   69 
   70 #endregion

Merk at du må inkludere System.Linq for å få tilgang til metodene Where() og Last().

For å gjøre testloggen enda enklere og mere beskrivende å bruke i test-sammenheng, har jeg lagt til noen funksjonelle godbiter for å gjøre verifiseringer. Den første metoden verifiserer at alle eventene i loggen tilfredstiller et gitt predikat, den andre at minst ett event gjør det (First() kaster et Exception om ingen matcher). Disse metodene har navn som passer inn med testmetodene fra coretdd som jeg alltid bruker.

   72 #region Asserts
   73 
   74 /// <summary>
   75 /// All events in log must satisfy the predicate
   76 /// </summary>
   77 public static void ShouldSatisfy(Predicate<LoggingEvent> predicate)
   78 {
   79     _log_events.ForEach(e => 
   80     {
   81         if (!predicate(e))
   82             Assert.Fail(
   83                 "Event does not satisfy predicate: {0} - {1}", 
   84                 e.Level, 
   85                 e.RenderedMessage);
   86     });
   87 }
   88 
   89 public static void ShouldContain(Func<LoggingEvent, bool> predicate)
   90 {
   91     _log_events.First(predicate);
   92 }
   93 
   94 #endregion

Merk at du må legge til en referanse til NUnit for bruken av Assert.Fail() i ShouldSatisfy-metoden.

Her er et par eksempler på hvordan disse siste metodene kan brukes:

   89 private void log_should_not_contain_any_fatal_events()
   90 {
   91     TestLog.ShouldSatisfy(logEvent => logEvent.Level != Level.Fatal);
   92 }
   93 
   94 public void log_should_contain_text(string value)
   95 {
   96     TestLog.ShouldContain(logEvent => logEvent.RenderedMessage.Equals(value));
   97 }

Håper dette kan være til nytte for noen. Lykke til med testingen!


comments powered by Disqus