č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

Žádné komentáře:

Okomentovat