Tag Archives: teszt

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.

Mutasd az importjaidat, megmondom, ki vagy

Na jó, azt talán nem, de arra utalhat, hogy mennyire karbantartható a kódod.

Írtál-e már ilyet:

import { SomeClass } from 'somelib';
...
const s = new SomeClass();

Vagy épp ilyet:

import * as someFunc from 'somelib';
...
const s = someFunc();

Ismerős, ugye? Én is rengetegszer írtam le hasonló sorokat, már csak azért is, mert minden könyvtár, library, package README-je pontosan ezt ajánlja. Van ezzel a mintával azonban (legalább) két probléma, ami miatt egy ideje igyekszem kerülni.

Függőségek kezelése

Ha gátlástalanul követed ezt a mintát, egyetlen alkalmazáson belül ugyanazt a sort le fogod írni százszor (vagy még többször). Miért? Mert viszi a kezed a copy-paste. Egyszerű, kényelmes, működik, nem igaz?

Így viszont nagyon nehéz lesz meghatározni, hogy a Te alkalmazásodnak pontosan mire is van szüksége, mitől függ, mi a dependenciája. A kódból mindössze annyi fog látszódni (de csak miután a kedvenc editorod “Find in All Files” funkciójával rákeresel), hogy a te kódod számtalan helyen hivatkozik egy külső csomagra, tehát az a csomag kell neki, függ tőle.

A valóság azonban más: a te alkalmazás logikádnak nem egy csomagra van szüksége, hanem egy funkcióra, egy logikára, amit történetesen egy külső csomagban implementált valaki más. Csakhogy ez most abszolút nem látszik a kódodból.

Íme egy konkrétabb példa a népszerű lodash csomaggal (*):

var _ = require('lodash');

Ez a csomag számtalan segédfüggvényt tartalmaz, szinte kizárt, hogy a te alkalmazásodnak mindegyikre szüksége van. Hogyan derítheted ki, hogy pontosan mit használsz? Rákeresel az összes fájlodban arra, hogy “_”.

De miért érdekes ez?

Például mert előfordulhat, hogy a hivatkozott csomagot le akarod cserélni valami másra. A nyílt forráskódú világban csomagok jönnek, mennek, ma még ez volt a legjobb, holnap már az lesz. Mert a régiben van egy bug, amit a fejlesztője már sosem fog kijavítani, vagy mert csak az új kompatibilis a legújabb Node verzióval, ami meg kell az appod többi részének. Vagy egyszerűen azért, mert frissen akarod tartani az appod kódját, és csak olyan dependenciákat szeretnél, amik körül aktív a közösség.

Szinte biztos, hogy nem fogsz találni egy másik könyvtárt, csomagot, ami API szinten kompatibilis a régivel. Olyanra van szükséged, ami funkció szinten kompatibilis, amit csak akkor tudsz kideríteni, ha pontosan ismered, hogy milyen funkcióra hivatkozol.

Ha ez megvan, akkor már csak az kell, hogy a lehető legkevesebb helyen kelljen átírni a hivatkozást a kódodban.

Ebből rögtön következik is a megoldás: törekedj arra, hogy a külső csomagokra való hivatkozásokat pontosan egyetlen helyen írd le a kódodban, azaz csomagold be őket egy osztályba, ami csak azokat a funkciókat publikálja, amire a te appod épít. Ez a módszer ráadásul olvashatóbbá is tudja tenni a kódodat, például “sha256()” helyett sokkal jobb függvény nevet is ki tudsz találni, igaz?

Ez a módszer nekem nagyon bevált, bár megjegyzem, nem követem fanatikusként, mert a framewörk szintű libek (pl. Angular) csomagolása túl nagy overhead lenne.

Tesztelhetőség

Ha egy osztályod importon keresztül hivatkozik egy másikra, akkor annak az osztálynak a unit tesztelése rémálom lesz, mert a hivatkozott osztály mockolásához a fájl betöltő logikát kell meghekkelni (például Mocking Request-Promise).

A megoldás nem újdonság: DI. Nem csak azt nyerjük vele, hogy könnyebb a tesztelés, de elég ránézni tipikusan a konstruktorra, és messziről látszik, hogy az adott osztály milyen más osztályoktól függ.

TypeScriptben például sokszor importálunk interface-eket csak azért, hogy legyen típus leírónk, ami miatt nagyon el tudnak szaporodni az import sorok, és gyakran nem látszik ránézésre, hogy melyik import hoz be funkcionalitást, és melyik csak típus információt. Ezen is segít a DI, ha törekszünk arra, hogy az importot csak típus leírásokhoz használjuk, minden másra ott a DI.

Mi ebben az újdonság?

Semmi az ég világon. Tényleg. Ennek ellenére mégsem akarnak kipusztulni ezek a kódok. A README-k, tutorialok, cikkek, blogok mindig az egyszerűségre fognak törekedni, olyan példákat fogsz bennük találni, amik segítenek a megértésben, az elindulásban. Nem az a céljuk, hogy vakon kövesd őket egy komolyabb alkalmazásban, ahol neked a karbantarthatóság és fenntarthatóság legalább olyan fontos, mint az egyszerűség. Amikor mégis leírsz egy ilyen sort legközelebb, gondolj arra, hogy ezzel mit veszel a nyakadba.

programming-is-thinking

 

*: A példa kicsit sántít, mert a lodash lehetővé tesz finomabb importokat is, csak épp kevesen használják.

 

Technorati-címkék: ,,

Kód futtatása tesztelés elején és végén

Aki Visual Studioban írt már unit teszteket, biztosan találkozott a [TestClass] és [TestMethod] attribútumokkal, amelyekkel a teszteket tartalmazó osztályokat és a konkrét teszteket meg tudjuk jelölni. Hasonlóképpen használhatunk attribútumokat olyan kódok jelölésére, amelyeknek a tesztek előtt vagy után kell lefutniuk:

A TestInitialize és TestCleanup attribútumokkal ellátott metódusok minden teszt előtt és után futnak le.

A ClassInitialize és ClassCleanup attribútumokkal ellátott metódusok az osztályban található első teszt futtatása előtt és az utolsó futtatása után futnak le.

Az AssemblyInitialize és AssemblyCleanup attribútumokkal ellátott metódusok a szerelvényben található első teszt futtatása előtt és az utolsó futtatása után futnak le.

Az utóbbiak különösen hasznosak tudnak lenni CI esetén.

 

Technorati-címkék: ,

JSON vagy nem JSON, ez itt a kérdés

Amikor egy REST API-hoz készítünk unit tesztet könnyen előfordulhat, hogy meg kell vizsgálnunk, hogy a szervertől kapott válasz olyan formában van-e, mint amiben kértük. Például egy stringről el kell döntenünk, vagy korrekt JSON-t tartalmaz-e.

Sok olyan megoldást lehet találni a neten, ami azt mondja, hogy elég, ha ellenőrizzük, hogy a kapott string < vagy { karakterrel kezdődik-e, hiszen a JavaScript Object Notation mindenképpen objektumokról vagy tömbökről szól. Ezzel a módszerrel nem csak az a baj, hogy egyetlen karakterből aligha tudjuk megmondani, hogy érvényes JSON stringről van-e szó, hanem az is, hogy érvényes lehet az a string is, ami nem tömböt vagy objektumot, hanem csak egy egyszerű értéket tartalmaz. Ami pedig a json.org szerint lehet:

json-value

A fapados karakter vizsgálgatás helyett szükségünk lenne egy korrektebb IsValidJson függvényre. Sajnos ilyet sehol nem találtam készen, de a Newtonsoft JSON.NET könyvtárral sikerült így megoldani a problémát:

try
{
    JToken.Parse(input);
    return true;
}
catch (JsonReaderException)
{
    return false;
}

Van jobb ötlete valakinek?

 

Technorati-címkék: ,,