čtvrtek 24. listopadu 2011

Rhino Mocks & spol. aneb imitujte rozhraní

Frameworky plné falše, náhražek, imitací a podvrhů ...

... a proto je máme tak rádi a proto je ke svému vývojářskému životu potřebujeme :-)

Nebojte se, jsme stále ještě ve světě férového programování. Tento příspěvek je krátkým motivačním úvodem do světa mock, fake, stub a dalších typů objektů, bez kterých se neobejde žádný vývojář, který to myslí vážně s psaním jednotkových (unit) a integračních testů.

Mock objekty a k čemu jsou dobré

Mock objekt vznikne jako fiktivní instance rozhraní nebo třídy. Této objektové imitaci pak můžete přiřadit chování, které očekáváte. Můžete také stnovit pravidla použití, které mohou být prověřeny na konci testu.

Předpokládám, že nejčastěji využíváte nebo budete využívat možnost "falešné" implementace rozhraní. V tomto případě oceníte techniku programování proti rozhraní (namísto programování proti implementaci). Je bežné, že třídy mají závislosti na jiných rozhraních. Pokud takovou třídu chcete pokrýt jednotkovými nebo integračními testy, musíte umět tyto závislosti vyřešit. Níže je uveden příklad který zastupuje tisícovku slov.

Většina mockovacích frameworků nabízí také validaci pořadí a počtu volání metod nebo přístupu k property mock objektu. Touto technikou můžete prověřit volání metod, které jsou pro potřeby testu důležité.

Rhino Mocks, Moq, NMock2, Isolator, TypeMock nebo Moles

Mockovacích frameworků pro .Net je několik. Liší se možnostmi toho, které třídy lze mockovat, intuitivností zápisu, možnostmi validačních pravidel, podporovanými verzemi .Net frameworků a dalšími aspekty. Přehledné srovnání je k dispozici na webu PHP vs .Net.

Nejrošířenějším volně dostupným frameworkem je Rhino Mocks. Zápis pravidel mock objektů je natolik intuitivní a variabilní, že pokryje většinu Vašich potřeb. Já osobně používám právě Rhino Mocks. Otázkou je, zda-li autor bude i nadále funkcionalitu rozvíjet. Pokud se nepletu, tak poslední update je někdy z roku 2009.

Ještě mám osobní zkušenost s NMock2. Pro mě je nevyhovující z toho důvodu, že názvy metod a vlastností se zapisují textově. Nejen že takový zápis zdržuje, nedá se využít IntelliSense, ale navíc není bezpečný pro refaktoring.

Mockování pro začátečníky

Mějme rozhraní zadefinovaná takto:

namespace Firma.Bll
{
    /// <summary>
    /// Rozhraní pro práci s kurzy.
    /// </summary>
    public interface IKurzyBll
    {
        /// <summary>
        /// Vrátí hodnotu kurzu měny.
        /// </summary>
        /// <param name="kodMeny">Kód měny.</param>
        /// <param name="datumPlatnosti">Datum platnosti.</param>
        /// <returns>Hodnota kurzu bez omezení přesnosti.</returns>
        decimal VratitHodnotuKurzuMeny(string kodMeny, DateTime datumPlatnosti);
    }

    /// <summary>
    /// Rozhraní pro práci s měnami.
    /// </summary>
    public interface IMenyBll
    {
        /// <summary>
        /// Převede částku v měně na částku domácí měny.
        /// </summary>
        /// <param name="kodMeny">Kód měny.</param>
        /// <param name="datumPlatnosti">Datum platnosti.</param>
        /// <param name="castkaVMene">Částka v měně.</param>
        /// <returns>Částka převedená na domácí měnu s přesností na dvě desetinná místa.</returns>
        decimal PrevestCastkuNaDomaciMenu(string kodMeny, DateTime datumPlatnosti, decimal castkaVMene);
    }
}

Třída implementující rozhraní IMenyBll vyžaduje při vzniku předání implementace IKurzyBll:

namespace Firma.Bll.Impl
{
    /// <summary>
    /// Implementace práce s měnami.
    /// </summary>
    public class MenyBll : IMenyBll
    {
        private IKurzyBll KurzyBll { get; set; }
          
        /// <summary>
        /// Pomocí constructor injection je vložena závislost na IKurzyBll.
        /// </summary>
        /// <param name="kurzyBll">Práce s kurzy.</param>
        public MenyBll(IKurzyBll kurzyBll)
        {
            KurzyBll = kurzyBll;
        }
 
        public decimal PrevestCastkuNaDomaciMenu(string kodMeny, DateTime datumPlatnosti, decimal castkaVMene)
        {
            decimal hodnotaKurzu = KurzyBll.VratitHodnotuKurzuMeny(kodMeny, datumPlatnosti);
            decimal castkaVDomaciMene = Math.Round(castkaVMene * hodnotaKurzu, 2, MidpointRounding.AwayFromZero);

            return castkaVDomaciMene;
        }
    }
}

A teď přichází chvilka slávy pro Rhino Mocks. Správnost implementace IKurzyBll nás v případě testování třídy MenyBll příliš nezajímá. Ale přesto ji potřebujeme. Bez ní implementaci MenyBll neotestujeme. V produkčním běhu aplikace může být IKurzyBll implementováno nad webovou službou, databázovou tabulkou nebo jiným způsobem. Pro účely testu by však bylo náročné takovou implementaci připravit. Jednoduchým řešením je vytvoření mock objektu, který naučíme vracet kurz podle potřeby našeho testu. Celá myšlenka by měla být zřejmá z kódu testovací metody:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Rhino.Mocks;

namespace Firma.Bll.Impl.Test
{
    /// <summary>
    /// Unit testy pro třídu MenyBll.
    /// </summary>
    [TestClass]
    public class MenyBllTest
    {
        private const decimal HodnotaKurzuMenaEur = 23.530m;

        [TestMethod]
        public void PrevodCastkyEurTest()
        {
            // Repozitář pro správu mock objektů.
            MockRepository mocks = new MockRepository();

            // Vytvoření mock objektu pro rozhraní IKurzyBll.
            IKurzyBll kurzyBllMock = mocks.StrictMock<IKurzyBll>();

            string kodMeny = "EUR";
            DateTime datumPlatnosti = DateTime.Today;

            // Definice očekávaného chování.
            // Při volání VratitHodnotuKurzuMeny() s parametry "EUR" a dnešní datum 
            // vrací hodnotu 23.530m. Počet volání této metody není omezen.
            Expect.Call(kurzyBllMock.VratitHodnotuKurzuMeny(kodMeny, datumPlatnosti)).
                Return(HodnotaKurzuMenaEur).Repeat.Any();

            // Realizuje definici všech mock objektů.
            mocks.ReplayAll();

            // Vytvoření instance testované třídy s podvržením mock implementace IKurzyBll.
            MenyBll menyBll = new MenyBll(kurzyBllMock);

            decimal castkaEur = 10.50m;
            decimal ocekavanaCastkaCzk = 247.07m;
            decimal vracenaCastkaCzk = menyBll.PrevestCastkuNaDomaciMenu(kodMeny, datumPlatnosti, castkaEur);

            // Pokud je očekávaná částka různá od skutečně vrácené částky, 
            // dojde k výjimce a test skončí chybou.
            Assert.AreEqual(ocekavanaCastkaCzk, vracenaCastkaCzk);
        }
    }
}

Pokud patříte k vyznavačům TDD (vývoj řízeny testy), pak byste zřejmě postupovali tak, že ještě před vlastním implementováním metody PrevestCastkuNaDomaciMenu() napíšete tento a případné další testy. Tzn. zadefinujete očekávané cílové chování metody formou unit testů. Následná korektní implementace zajistí, že všechny testy začnou procházet.

Několik odkazů pro rychlejší rozjezd

čtvrtek 17. listopadu 2011

NConfig - řešení pro lokální konfigurace

Proč se nám může NConfig hodit

Přijdete ráno do práce, zvolíte ve Visual Studiu Get Latest Version nad celým projektem (řešením) a jdete si uvařit kafe. Pokračujete v implementaci nové funkcionality a spouštíte testy nad databází. Začaly se však objevovat chyby, které jsou hodně podezřelé. Vypadá to jako by v příslušné databázi nebyly struktury, které jste si vytvořili nově v rámci vývoje. Pátráte, jak je to možné a ztrácíte drahocené minuty. Když už začínáte být trochu zoufalí, uvědomíte si, že jste si aktualizovali lokální workspace. Podíváte se do konfiguračního souboru a zjistíte, že někdo změnil směrování na databázi! V historii změn zjistíte, že to byl Peter. V duchu si zanadáváte a zároveň si uvědomíte, že už jste párkrát udělali kolegům to samé. Omylem jste dali vrácení změn na server (Check-in) ve společném konfiguračním souboru (Web.config, App.config). Pokud však máte systémový přístup k řešení problémů, pokusíte se toto neustálé přepisování nějak elegantně vyřešit.

A možná by Vám mohl pomoci NConfig!

Jak získat NConfig

NConfig je .Net knihovna, jejíž použití ve Vašich projektech není nijak licenčně omezeno. Projekt je vyvíjen na serveru GitHub na adrese https://github.com/Yegoroff/NConfig.

Já jsem postupoval tak, že jsem si přes odkaz Download stáhnul celý adresář jako zip soubor. Následně jsem jej rozbalil, otevřel solution NConfig.sln a sestavil z projektu NConfig výslednou dll knihovnu ve verzi pro framework 4.0 (k dispozici je i projekt pro framework 3.5). Dále jsem již pracoval pouze s dll knihovnou.

Co NConfig umí

NConfig umí slučovat konfigurace z více konfigračních souborů. Umí také za běhu vybrat konfigurační soubor podle názvu aktuálního počítače. Tato funkcionalita je řešením pro výše uvedený motivační případ.

// Sloučí nastavení defaultního konfiguračního souboru a 
// Configs\Custom.config, resp. Configs\{NazevPocitace}.Custom.config
NConfigurator.UsingFiles(@"Configs\Custom.config").SetAsSystemDefault();

Autor tvrdí, že NConfig lze využít v aplikacích typu ASP.Net, ASP.Net MVC, WinServies, WinForms, WPF a konzolová aplikace.

Ukázka použití

Požadujeme, aby si vývojáři Peter a Steve mohli nastavit navzájem nezávislé lokální konfigurace pro připojení k databázím tak, aby nezasahovali do společného konfiguračního souboru.

Vytvoříme si jednoduchou konzolovou aplikaci a nareferencujeme NConfig.dll. Přidáme standardní App.config. Uživatelské konfigurační soubory umístíme do složky Configs. Ve složce vytvoříme defaultní Custom.config a konfigurační soubory pro Petera a Steva - PeterComputer.Custom.config a SteveComputer.Custom.config. Viz. obrázek. Nezapomeňte nastavit pro konfigurační soubory ve složce Configs vlastnost Copy To Output Directory na true.

Soubor App.config vypadá takto:

<?xml version="1.0"?>
<configuration>
 <appSettings>
  <add key="Database" value="AppDatabase"/>
  <add key="User" value="AppUser"/>
 </appSettings>
</configuration>

Soubor Custom.config vypadá takto:

<?xml version="1.0"?>
<configuration>
 <appSettings>
  <add key="User" value="CustomUser"/>
 </appSettings>
</configuration>

A například soubor PeterComputer.Custom.config vypadá takto:

<?xml version="1.0"?>
<configuration>
 <appSettings>
  <add key="Database" value="PeterComputerDatabase"/>
  <add key="User" value="PeterComputerUser"/>
 </appSettings>
</configuration>

Třída Program konzolové aplikace využije volání metody UsingFiles() třídy NConfig.NConfigurator, která sloučí původní App.config s Configs\Custom.config, případně s konfiguračními soubory podle spuštěného počítače:

class Program
{
    static void Main(string[] args)
    {
        // SwitchOnCustomConfig();

        Console.WriteLine(String.Format("User='{0}'", ConfigurationManager.AppSettings["User"]));
        Console.WriteLine(String.Format("Database='{0}'", ConfigurationManager.AppSettings["Database"]));
    }

    static void SwitchOnCustomConfig()
    {
        NConfigurator.UsingFiles(@"Configs\Custom.config").SetAsSystemDefault();
    }
}

Přehled scénářů:

Scénář Konfigurační soubor Hodnota klíče Database Hodnota klíče User
Není zapnutá podpora volitelných konfiguračních souborů - nevolá se metoda SwitchOnCustomConfig(). App.config AppDatabase AppUser
Je zapnutá podpora volitelných konfiguračních souborů - volá se metoda SwitchOnCustomConfig() a aplikace není spuštěna ani na počítači Petera ani Steva. Custom.config AppDatabase CustomUser
Je zapnutá podpora volitelných konfiguračních souborů - volá se metoda SwitchOnCustomConfig() a aplikace je spuštěna na počítači Petera. PeterComputer.Custom.config PeterComputerDatabase PeterComputerUser

neděle 13. listopadu 2011

Pravidla pro pojmenování objektů v C# - část 1

Problematika je pro svoji obsáhlost rozdělena do více příspěvků:

  1. Úvod, jazyk, názvy tříd
  2. Názvy vlastností (properties), polí (fields) a proměnných (variables)
  3. Názvy metod a argumentů
  4. Další pravidla, názvy balíčků, testovací třídy

Úvod

U programového kódu se předpokládá, že splňuje syntaktickou a sémantickou správnost. Tyto dva požadavky jsou nutné pro vlastní fungování výsledné aplikace. Aby jste kód mohli efektivně udržovat a rozvíjet, je neméně důležitá dostatečná čitelnost kódu a správné názvosloví. Možná se Vám již někdy stalo, že jste se vrátili k Vašemu staršímu kódu a dlouze jste se snažili vyčíst, co jste takovým zápisem vlastně sledovali. Chvíli sami sebe přesvědčujete, že toto nemůže být Váš kód, takhle nepřehledně byste to přeci nikdy nenapsali. Nakouknete do historie v repository a zjistíte, že jste to byli opravdu Vy. Trochu se zastydíte a slíbíte si, že příště si na kódu dáte více záležet.

Pokud se dostanete ke kódu převzatému od nějakého "kouzelníka" (v některých firmách se těmto lidem nesprávně říká guru), může se situace ještě více zdramatizovat. Mnohdy se stává, že některé části kódu pro Vás zůstanou zapovězeny navždy a Vy se smíříte s tím, že kód sice něco dělá, ale netušíte jak. Zasáhnout do takového kódu vyžaduje dostatek osobní odvahy a pokud nemáte dostatečné pokrytí kódu testy, můžete úpravou nadělat nevědomky pěknou paseku.

Jak se naučíte psát čitelný a dobře spravovatelný kód? Pouze praxí, znalostí níže uvedených pravidel, učením se z vlastních i cizích chyb, skupinovým posuzováním kódu (code review). V tomto miniseriálu o názvosloví se Vám pokusím ukázat několik pravidel, které doporučuje odborná literatura, a které se mi osvědčily ve vlastní praxi.

Jazyk

Na začátku projektu se musíte rozhodnout jaký jazyk použijete. Nejedná se o jazyk programový, ale lingvistický. Svět programování mluví anglicky, ale přítomnost mateřské češtiny nemusí být na škodu. Většina z nás má lepší vyjadřovací schopnosti v češtině, angličtina je však mnohdy výstižnější a nedává nám příliš velký manévrovací (dezinformační) prostor. Záleží také na jazykovém složení Vašeho týmu. Pokud děláte lokální projekty s homogenním českým týmem (mohou být přimícháni i slovenští kolegové), můžete využít češtinu. V případě jazykově heterogenního týmu na výběr nemáte a budete zřejmě komunikovat a pojmenovávat programové objekty v angličtině. Pokud se Vám ovšem nepodaří husarský kousek, kdy naučíte třeba němce spisovné češtině ;-)

Můžete se také rozhodnout, že některé nejnižší vrstvy programového systému budou čistě anglické a vyšší vrstvy naopak české. Například jedna podvrstva datové vrstvy komunikující s nějakým persistentním frameworkem bude anglická a vrstvy od aplikační logiky (business logic layer) výše (vrstva služeb, prezentační vrstva) budou české. Především ve vrstvě aplikační logiky můžete s výhodou využít češtiny a její (pro české vývojáře) přirozené srozumitelnosti. Samozřejmě za dodržení určitých pravidel a jednotnosti. Toto kombinování jazyků však nedoporučuji.

Určitě se však vyhněte tomu, že v rámci jedné třídy budete mít namíchány názvy z více jazyků. Výjimku tvoří metody a property, které dědí Vaše třída. Např. třída pojmenovaná jako Objednavka může přepisovat metodu GetHashCode() nebo Equals(). Podle stejné logiky nemíchejte jazyky v rámci jednoho jmenného prostoru nebo ještě lépe v rámci celé assembly.

Jazyk použitý pro názvosloví programových objektů by měl korespondovat s jazykem použitým pro objekty v persistentním úložišti (v databázi). Jednoduše řečeno by mělo platit, že tabulky a sloupce jsou pojmenované ve stejném jazyce jako odpovídající třídy a vlastnosti.

V dalším textu budu používat názvosloví v češtině a budu předpokládat, že většina pravidel se dá aplikovat i na angličtinu.

Názvy tříd

  1. Používejte PascalCase notaci. V C# se nepíše polozkaObjednavky, ale PolozkaObjednavky. Omlouvám se tomu, koho jsem takovou samozřejmostí urazil ;-)

  2. Používejte jednotné číslo. Instancí Vaší třídy je jeden objekt. Název třídy Objednavky proto nedává smysl. Správně je Objednavka.

  3. Název by měl být významový a jednoznačný. Pokud v systému zadefinujete třídy Objednavka, DataObjednavky a ObjednavkaInfo zaděláváte si na problémy. Na první pohled není zřejmé, v čem se třídy liší. Pokud všechny reprezentují objektovou entitu odpovídající objednávce v reálném světě, jedná se o chybnou duplicitu v objektovém návrhu. Pokud tomu tak není a např. třída DataObjednavky umí nějakou obchodní logiku, např. vrací určité statistiky nad objednávkami, pak je jistě název takové třídy zvolen chybně.

  4. Používejte názvy z domény řešeného problému. Od doménového experta by návrhář systému (alias implementátor) měl dostat doménový slovník, který definuje pojmy a pravidla, které má aplikace pokrýt. Každá doména má svoji terminologii, která je standardizovaná a zúčastnění jí rozumí. Musíte těmto pojmům rozumět také a držet se dané terminologie v názvech tříd a vlastností.

  5. Používejte vhodné a standardizované přípony. Je běžné, že se v systému objevuje více tříd související s danou entitou. V takových případech je vhodné standardizovat určité přípony a zadefinovat jejich jednoznačný význam. Podívejme se na příklad objednávky a s ní souvisejících tříd:

    • Objednavka [datová vrstva] - třída reprezentující objednávku a její vlastnosti (mapovány na databázové sloupce tabulky s objednávkami).
    • ObjednavkaDao [datová vrstva] - třída odpovědná za CRUD (Create, Read, Update, Delete) operace, např. načtení objednávky podle jejího Id.
    • ObjednavkaBll [vrstva obchodní logiky] - třída zajišťující realizaci obchodních pravidel pro práci s objednávkami, např. vykrytí objednávky.
    • ObjednavkaDto [vrstva služeb] - zjednodušená třída (jejíž objekty jdou serializovat) pro přenos přes vrstvu služeb u vícevrstvých aplikací.
    • ObjednavkaSluzba [vrsta služeb] - třída nabízející logiku práce s objednávkami na úrovni komunikační vrstvy.
    • ObjednavkaObsluha [prezentační vrstva] - třída plnící funkci presenteru v MVP nebo controlleru v MVC návrhovém vzoru pro prezentační vrstvy.
    • ObjednavkaPohled [prezentační vrstva] - třída zajišťující vykreslení objednávky v závislosti na vybrané prezentační technologii.

  6. Nepoužívejte v názvech názvy objektových typů. Pokud třída řeší překlad českých slov na německá vnitřně pomocí hešovací tabulky, nepoužívejte název CeskoNemeckySlovnikHashTable, ale pouze CeskoNemeckySlovnik. Klienta třídy nezajímá její vnitřní implementace. Je pro něj důležitá pouze veřejná část třídy.

  7. Nezkracujte na úkor čitelnosti. Pokud je to vhodné použijte víceslovný název třídy, který přesněji vymezí význam třídy. V době pomůcek pro efektivitu psaní kódu typu IntelliSense se dlouhých názvů bát nemusíte.

    Pozor na situace, kdy se nedá význam třídy popsat jednoduše. V těchto případech se může jednat o blikající kontrolku chybného návrhu třídy, konkrétně o porušení pravidla jedné zodpovědnosti. Např. třída AdresaABankovniUcetZamestnance zřejmě slučuje dvě věci (adresu a bankovní účet), které by měly být odděleny do dvou samostatných tříd.

  8. Nebojte se refaktorovat název. Nemusíte ideální název třídy vymyslet hned napoprvé. Ale pokud časem přijdete na výstižnější název, nebojte se jej refaktorovat. Kód musíte neustále vylepšovat.

  9. Nepoužívejte slovesa v názvech. Třída ZamestnanecMajiciDohodu má možná literárně hodnotný název, ale do názvu třídy něco takového nepatří. V tomto případě se zřejmě jedná navíc o chybný objektový návrh a chybnou dědičnost.