Singleton unit tesztelése, avagy .NET-ben miért nem 12 hónapból áll az év?

Íme mai vizsgálódásunk tárgya:

public class Calendar
{
  private static Calendar _instance;

  public static Calendar Instance { ... }

  private Calendar() { }

  public string[] GetMonthNames()
  {
    string[] names = DateTimeFormatInfo.CurrentInfo.MonthNames;
    return Sorter.Alphabetically(names);
  }

  // És még sokan mások...
}

Két dolgot mindenképp érdemes ezen az osztályon megfigyelni: van egy GetMonthNames metódusa és követi a Singleton tervezési mintát. A kettő nyilván összefügg, a metódust Calendar.Instance.GetMonthNames() formában lehet meghívni.

Mivel ez egy nagyobb, korosabb kódbázis része, mielőtt nagyobb változtatásokba fogunk, írjunk unit teszteket a GetMonthNames függvényhez. És itt rögtön belefutunk abba a problémába, hogy nem tudjuk izolálni a metódust, bele van drótozva, hogy függ a DateTimeFormatInfo és a Sorter osztályoktól. Az előbbi a .NET Framework része, az utóbbit viszont én találtam ki, ugyanebben a projektben van:

public static class Sorter
{
  public static string[] Alphabetically(string[] values)
  {
    return values.OrderBy(v => v).ToArray();
  }
}

Alakítsuk át, de úgy, hogy a Calendar osztályban lévő 123 másik metódust, és az őket hívó kódot ne kelljen módosítani.

A függőséget írjuk le egy interfésszel, mert azt könnyű lesz mockolni:

public interface ISorter
{
  string[] Alphabetically(string[] values);
}

Implementáljuk úgy, hogy visszavezetjük a már meglévő, static megvalósításra:

public class SorterWrapper : ISorter
{
  public string[] Alphabetically(string[] values) => Sorter.Alphabetically(values);
}

Másként fogalmazva eddig annyit csináltunk, hogy a static implementációt becsomagoltuk egy példányosítható implementációba, amit egy interfésszel is le tudunk írni.

Mivel az eredeti Calendar osztály nyilvános interfészéhez nem akarunk hozzányúlni, vezessünk be újabb osztályt, amit én fantáziadúsan MonthManager-nek neveztem el:

public class MonthManager
{
  private ISorter _sorter;

  public MonthManager(ISorter sorter)
  {
    this._sorter = sorter;
  }

  public string[] GetMonthNames()
  {
    string[] names = DateTimeFormatInfo.CurrentInfo.MonthNames;
    return this._sorter.Alphabetically(names);
  }
}

Ez már egy kiválóan tesztelhető osztály, aminek a függőségét a konstruktoron keresztül meg lehet adni. (Az egyszerűség kedvéért a DateTimeFormatInfo függőséggel itt szándékosan nem foglalkozom.)

Módosítani kell természetesen az eredeti Calendar osztályt is, de szerencsére csak a belső implementációját, a publikus interfésze változatlan marad:

public class Calendar
{
  // Singleton...

  private MonthManager _monthManager;

  private Calendar()
  {
    ISorter sorter = new SorterWrapper();
    this._monthManager = new MonthManager(sorter);  
  }

  public string[] GetMonthNames()
  {
    return this._monthManager.GetMonthNames();
  }
}

Ehhez már könnyen írhatunk teszteket:

public class IdentitySorter : ISorter
{
  public string[] Alphabetically(string[] values) => values;
}

[TestClass]
public class CalendarTests
{
  [TestMethod]
  public void ShouldReturnTwelveMonths()
  {
    MonthManager mgr = new MonthManager(new IdentitySorter());
    string[] months = mgr.GetMonthNames();
    Assert.AreEqual(12, months.Length);
  }
}

Igazán kár, hogy ez a teszt hibát fog jelezni, méghozzá mindig! Látod a hibát?

Nem? Mert nincs is! Ez feature 🙂

A .NET Frameworkben lévő System.Globalization.Calendar osztályt úgy tervezték, hogy mindenféle naptárt le tudjon írni, van is 11 származtatott osztálya, köztük például a GregorianCalendar. Természetesen az egyes naptárak máshogy értelmezhetik a hónap fogalmát és ennek megfelelően más lehet a számuk is. Ennek a támogatását pedig úgy sikerült megoldani, hogy a DateTimeFormatInfo osztály MonthNames tulajdonsága CurrentCulture-tól függetlenül mindig 13 (!) elemű tömböt ad vissza, ami a mi naptárunk esetén a 12 hónap nevét jelenti, a 13. elem pedig egy üres string. Mindezért persze nem a .NET a felelős, így van ez a Win32 API-ban is (lásd LOCALE_SMONTHNAME13 konstans) már jó rég óta.

Leave a comment