Avhengighetsisolering (a.k.a. Mocking) i .NET


torsdag 27. august 2009 Software/verktøy Testing / TDD

mocking_frameworks I min forrige blogpost, om utenfra-og-inn programmering, snakket jeg om at mocking er en teknikk vi bruker når vi praktiserer testdreven utvikling. For å kunne utvikle én og én enhet om gangen er det fordelaktig å kunne erstatte enhetens avhengigheter med "jukseobjekter".

Roy_Pic_BW_Small Enhetstest-guru Roy Osherove (som jobber for TypeMock) påpeker at vi ofte misbruker begrepet mocking. Martin Fowler har også skrevet en mye lest artikkel kalt Mocks Aren't Stubs om dette: En MOCK er ifølge dem et objekt som brukes til å registrere og validere forventninger, mens en STUB er et objekt som inneholer predefinerte svar. FAKE er et objekt med en faktisk implementasjon, som for eksempel en in-memory database som erstatter en faktisk database. Vi bruker normalt en blanding av disse i testene våre, og jeg ser ikke noen spesiell grunn til å kunne skille mellom dem.

Roy anbefaler å bruke begrepet avhengighetsisolering for å samle disse teknikkene, og kaller TypeMock for et avhengighetsisoleringsrammeverk (det ble veldig fint på norsk gitt). Jeg bruker likevel mocking-begrepet, siden det er mest kjent – alle forstår hva jeg mener da.

Uansett hva man kaller dem – fordelen med disse falske objektene er at de er til å stole på. Mens faktiske implementasjoner kan gi ulike svar (basert på eksterne data, randomness etc.) kan vi fullstendig kontrollere hvordan en fake skal oppføre seg i enhver situasjon. For eksempel kan vi simulere ulike unntasksituasjoner ved å få dem til å kaste exceptions på kommando.

Testene blir i mange tilfeller også enklere å skrive, gitt at du behersker et godt mockingrammeverk. Testene kan dessuten eksekvere raskere, fordi avhengighetene blir "lettere" enn de faktiske avhengighetene.

Litt kontroverielt

Jeg føler meg forpliktet til å nevne at det er en viss diskusjon rundt bruk av mocking. Det er mange som hevder at mocking er unødvendig, og kanskje til og med uønsket, om man designer systemet sitt "riktig". Jeg var selv av den oppfatning for ikke så alt for lenge siden. Men etter å ha forsøkt det en stund endret jeg mening.

Likevel innser jeg at mocking kan missbrukes – spesielt de kraftigste rammeverkene. Når man skriver tester er det viktig å teste adferd, ikke implementasjon – knytter man dem for hardt mot implementasjonen blir testene sårbare for endringer i systemet. Du kan få en grei oversikt over denne debatten ved for eksempel å lese et par sentrale spørsmål på stackoverflow: Why do I need a mocking framework for my unittests? | The value of high level unit tests and mock objects | Should one test internal implementation, or only test public behaviour?

Resten av denne blogposten er ment å gi en oversikt over hvilke muligheter du som utvikler på .NET-plattformen har for å praktisere avhengighetsisolering.

Manuell mocking

La oss først se på hvordan manuell gjør-det-selv-mocking fungerer. Det kan være lurt å starte der, for å få en grunnleggende forståelse for hvordan og hvorfor man mocker. La oss si at jeg skal teste en LoginController-klasse. Den har to avhengigheter; et LoginView og en UserRepository.

    1 [Test]

    2 public void Should_login_user_successfully()

    3 {

    4     var view = new FakeLoginView();

    5     new LoginController(view, new FakeUserRepository());

    6     view.TriggerAuthenticationRequested();

    7     Assert.IsTrue(view.hidden);

    8     Assert.IsTrue(view.Result == AuthenticationResult.Ok);

    9 }

Etter best practises injectes avhengighetene inn gjennom konstruktøren til kontrolleren – dette gjør det mulig for oss å mocke avhengighetene gjennom arv. I testen oppretter jeg stubs som jeg bruker til å simulere at en bruker trigger en authentiserings-request, og til å sjekke at resultatet av authentiseringen er at viewet skjules og Result-propertien settes til OK.

De to stub-klassene, som jeg koder spesifikt for denne testen, ser slik ut:

    1 class FakeLoginView : LoginView

    2 {

    3     // LoginView Members

    4     public event EventHandler AuthenticationRequested;

    5     public string Username { get { return "torbjorn"; } }

    6     public string Password { get { return "passw0rd"; } }

    7     public AuthenticationResult Result { get; set; }

    8     public void Hide() { hidden = true; }

    9     // end LoginView Members

   10 

   11     public FakeLoginView() { Result = AuthenticationResult.Pending; }

   12     public virtual void TriggerAuthenticationRequested()

   13     {

   14         if (AuthenticationRequested != null)

   15             AuthenticationRequested(null, EventArgs.Empty);

   16     }

   17     public bool hidden;

   18 }

   19 

   20 class FakeUserRepository : UserRepository

   21 {

   22     public User FindByName(string username)

   23     {

   24         return new User {

   25             PasswordHash = "BED128365216C019988915ED3ADD75FB" };

   26     }

   27 }

Stub'ene er spesialdesignet for å gi den adferden jeg trenger i testen; FakeLoginView returnerer et predefinert brukenavn og passord, og har en variabel jeg kan sjekke mot for å finne ut om Hide-metoden har blitt kalt. FakeUserRepository vil uansett input returnere en bruker med et passord-hash som korresponderer med det predefinerte passordet.

Det er endel arbeid å lage disse stub'ene, men det er ikke så vanskelig. Problemet er at slik de er nå er de bare anvendelige for et fåtall tester – for andre tester som skal simulere andre ting må jeg skrive nye stubs (eller utvide disse med mer logikk). Jo flere stubs man må lage, jo mer kompleksitet innfører man i test-suiten.

Moq

Moq er et relativt nytt, open source mockingrammeverk for .NET, utviklet av Daniel Cazzulino (@kzu). Rammeverket er designet med tanke på enkelhet, er basert på Castle DynamicProxy, og er min klare favoritt. Jeg har nylig innført Moq på jobben, og det virket ikke som om utviklerne hadde store problemer med å ta det i bruk.

Moq bruker ikke Record/Replay-semantikken som flere andre rammeverk er kjent for, det har en mere intuitiv Arrange-Act-Assert approach. Eneste potensielle ulempen med rammeverket er at du må bruke .NET 3.5 (eller høyere).

Tilsvarende test som den over blir som følger ved bruk av moq:

    1 [Test]

    2 public void Should_login_user_successfully()

    3 {

    4     var view = new Mock<LoginView>();

    5     view

    6         .Setup(v => v.Password)

    7         .Returns("passw0rd");

    8 

    9     var repository = new Mock<UserRepository>();

   10     repository

   11         .Setup(r => r.FindByName(It.IsAny<string>()))

   12         .Returns(new User { PasswordHash = "BED128365216C019988915ED3ADD75FB" });

   13 

   14     new LoginController(view.Object, repository.Object);

   15     view.Raise(v => v.AuthenticationRequested += null, EventArgs.Empty);

   16     view.Verify(v => v.Hide());

   17     view.VerifySet(v => v.Result = AuthenticationResult.Ok);

   18 }

I linje 4 til 12 setter jeg opp mockene (Arrange): Jeg lager et view som skal returnere et predefinert passord, og en repository som skal returnere et predefinert passord-hash. I linje 14 sender jeg inn mockene til controlleren, og i linje 15 trigger jeg autentiseringforespørselen (Act). I linje 16 verifiserer jeg at Hide-metoden har blitt kalt, og linje 17 sjekker at Result har blitt satt til OK (Assert).

Moq er under stadig utvikling, og virker ganske populært for tiden. Daniel lanserte for eksempel nettopp Linq to Mocks – en helt ny approach for å opprette mock-objekter i form av en spørring. Det eksisterer også et moq-contrib prosjekt hvor du finner støtte for Automocking (rekursiv oppretting av mocks – innebærer mindre Arrange-kode i testene). OPPDATERING: Rekursiv mocking har blitt flyttet fra moq-contrib til å bli del av moq i versjon 2.6.

TypeMock Isolator

TypeMock Isolator er det aller kraftigste mockingrammeverket vi har tilgjengelig – det kan gjøre ting ingen av de andre kan, som å mocke ikke-virtuelle metoder, statiske klasser og klasser merket som sealed. Ulempen er at det koster penger.., Ganske Mange Penger!

Mange hevder også at styrken dette rammeverket tilbyr lett kan føre til dårligere design, og det er jeg enig i. Vi tok i bruk TypeMock i min forrige jobb, og der førte det til at vi lagde dårlige og komplekse tester som var mer eller mindre umulige å vedlikeholde. Når man derimot allerede har et dårlig design som utgangspunkt – kanskje man har behov for å mocke deler av selve .net-rammeverket – så kan nok TypeMock være verdt sin vekt i gull. (Men hvor mye veier software egentlig?)

Rhino Mocks

Rhino Mocks er et open source rammeverk utviklet av Oren Eini (også kalt Ayende Rahien). Mens TypeMock er det kommersielle storebror med de kraftigste featurene, er Rhino Mocks det mest brukte mockingrammeverket blant .NET-utviklere. Tidligere hadde Rhino en lite intuitiv Record/Replay-syntax, men støtter nå Arrange-Act-Assert, og da ligner det i grunnen svært mye på Moq.

Rhino Mocks må sies å være et svært modent og fleksibelt rammeverk, og som Moq bygger det på Castle DynamicProxy. Den store fleksibiliteten kan være forvirrende, og dokumentasjonen kan være mangelfull. Dette veies opp ved at det har svært mange brukere, så noen andre har alltid gjort det du skal gjøre, og sansynligvis blogget om det.

Flere har utvidet Rhino Mocks til også å støtte automocking, bl.a. Jeremy D. Miller i sitt StructureMap-prosjekt. For det jeg vet kan det hende Ayende også har implementert dette i Rhino Mocks direkte by now.., man vet aldri hva den fyren får til på en ettermiddag.

Stubs

Microsoft Research har sluppet et rammeverk de kaller Stubs, som ble utviklet for deres nye testautomatiserings-verktøy Pex. Dette skiller seg ut fra de andre ved at det bruker kodegenerering. Det er alltid interessant å se hva som kommer ut av MS Research, og selv om jeg ikke vet om dette er interessant som en erstatning for Rhino eller Moq, så er det i alle fall en ny og spennende approach. Sjekk ut Getting Started with Stubs for en lynrask introduksjon.

Merk forøvrig at current versjonsnummer er 0.15.40714.1, så jeg vet ikke om dette er "produksjonsklart" for å si det sånn :)

NMock / Nmock2

Så vidt jeg vet var NMock det første mockingrammeverket på .NET plattformen, laget som en port av DynaMock fra Java. Versjon 2 av NMock ble utviklet av et annet team, og er implementert etter designet fra jMock. Interessen for disse rammeverkene har vært dalende, og det har flere svakheter i forhold til de mer moderne konkurrentene: Den viktigste forskjellen er at mockene ikke er typet ved compile time – man bruker "magiske strenger" for referanser, noe som kan være skummelt og vanskelig å vedlikeholde når ting i designet endres.

Er du interessert i å se nærmere på forskjellene på de fem rammeverkene jeg har nevnt sålangt – moq, TypeMock Isolator, Rhino Mocks, Stubs og NMock2 – så finnes det et prosjekt på Google Code du kan laste ned som heter Mocking Frameworks Compare. Her har du eksempeltester implementert vha. alle rammeverkene, i tillegg til en performance test som sammenligner dem.

Andre rammeverk

EasyMock er et mockingrammeverk fra Java, men det har blitt portet til .NET-plattformen under navnet EasyMock.NET. Det er egentlig alt jeg vet om dette rammeverket, jeg har ikke hørt om noen som bruker det, og det virker som om det ikke har blitt videreutviklet siden 2004, så det er neppe noe å satse på.

NUnit, "alles favorittrammeverk" for enhetstester, har også støtte for mocking – i NUnit-nedlastingen finner du nemlig en lite kjent dll som heter NUnit.Mocks. Jeg har lest at den har begrensede muligheter i forhold til andre rammeverk, men den kan kanskje være aktuell om man ikke skal ta i bruk avhengighetsisolering fullt ut, men har bruk for det i et par tilfeller, og i tillegg allerede bruker NUnit.

Så det var min oversikt over mockingrammeverk i .NET. Min neste blogpost er en praktisk toturial i utenfra-og-inn programmering hvor jeg kommer til å bruke mye mocking.., så følg med om du er interessert i det!

Til slutt: Har du erfaring med noen av disse rammeverkene er det veldig hyggelig om du legger igjen en kommentar med eventuelle mangler i blogposten. Jeg har ikke brukt alle disse vektøyene selv, og har ikke vært en aktiv "mocker" så veldig lenge. Sett meg på plass om du kan!

Knagger: , , , ,


comments powered by Disqus