BDD: Interfaces og Mocking
Efter at have fastlagt min adfærd, skal jeg have nogle ting på plads for at kunne komme videre med en faktisk implementering. Jeg har altid haft problemer med Interfaces - de har altid virket en smule overflødige, når nu man kan lave nedarvning istedet. For så kan man jo bare have en ’super’-klasse, som alle andre klasser arver fra… og så kalde implementationens properties og methods og bruge basisklassen som input-parameter. Lad os sige det kort - jeg har fået indsigt som gør, at jeg kan se, hvorfor det ikke er heldigt. Når man en gang er blevet nødt til at lave sin ’super’-klasse om… og gøre det stykvist… så har man ikke lyst til at gøre det igen. At omdøbe en klasse er nemt i Visual Studio… hvis det er alle referencer, der skal omdøbes…
Og for så at tage den ‘teoretiske’ tilgang - designet bør være uafhængig af den faktiske implementation, fordi man så kan skifte implementationen uden at man skal lave alt det andet om. Jeg har sat teoretisk i situationstegn, fordi det i høj grad også er summen af erfaringer gjort i den virkelige verden, som ligger til grund for teorierne på dette punkt.
Mit næste problem var efter, jeg stiftede bekendtskab med unit-test. Begreber som black-box, white-box, mocking og stubs er intellektuelt svære at koble sammen, når man ser dem i en kontekst, hvor man er fokuseret på at teste implementationen. Hvornår man anvender den ene i forhold til den anden - hvornår det giver mening at teste en metode særskilt, og hvorfor det overhovedet er interessant, når nu outputtet er resultatet af en string.Format(”{0}.{1}”,param1,param2).
Men lad os lade EconomyDeluxe tale som eksempel på, hvorfor mocking og interfaces går hånd i hånd, når adfærdsspecifikationer skal laves. Først skal jeg have lavet nogle interfaces for at kunne lave mine tests, som beskrevet i det forrige indlæg. Det virker rimelig klart at vi gerne vil vide om en FinansKonto er gyldig (IsValid) og den skal have et unikt nummer:
using System;
public interface ILedgerAccount
{int AccountId{get;}
bool IsValid{get;}}
Første anke kunne være, hvorfor jeg anvender en int som unik nøgle, og ikke en GUID; og hvorfor jeg ikke adskiller Id’et fra en kontonummeret, så jeg kunne have mulighed for at bruge en string - og hvad med, når det skal gemmes i en db. Svaret er faktisk rimelig simpelt - jeg ved ikke endnu om det er nødvendigt. YAGNI og KISS er to patterns som jeg er meget tilhænger af. Jeg er ved at beskrive adfærden - og hvad FinansBalancen skal vide om FinansKonto, for at kunne lave adfærdsspecifikationen - hvorvidt FinansKonti overhovedet skal gemmes i en database, har jeg slet ikke overvejet på nuværende tidspunkt.
FinansBalance er næste offer - vi ved at vi gerne vil kunne tilføje og slette FinansKonti. Og vi vil gerne kunne tælle hvor mange FinansKonti, der ligger i FinansBalancen - og sidst men ikke mindst vil vi gerne kunne finde ud af om en FinansKonto’s AccountId allerede er tilføjet - vi prøver:
using System;
using System.Collections;public interface IGeneralLedger
{void AddAccount(ILedgerAccount account);
void DeleteAccount(ILedgerAccount account);
IDictionary Accounts{get;}}
Igen har vi en situation, hvor mine valg måske ikke er umiddelbart gennemskuelige. Det ligger nok i kortene at vi skal have noget liste-agtigt at slå op i og tælle på. Grunden til valget af IDictionary er at det er den mest letvægts standardimplementation, som opfylder mine krav - havde jeg valgt IEnumerable (som er mest letvægts), mangler jeg både Count, Add og Delete - så jeg ville skulle lave disse funktioner særskilt - hvilket ville gøre IEnumerable et dårligt valg. ICollection (som er den næste i rækken af liste-implementationer) ville have givet mig Count - men jeg mangler stadig Add og Delete. Og så når vi til IDictionary eller IList - som har alt den funktionalitet, som jeg skal bruge her og nu. IDictionary vinder fordi det i implementationen ikke er muligt at tilføje den samme nøgle til samlingen - hvilket er en god sikkerhed i mit tilfælde. Opslag på nøgleværdien er desuden billigt, fordi jeg kan slå op i nøgleværdierne alene og jeg er ikke afhængig af at skulle sammenligne objekter på property-niveau.
Og til sidst grunden til at jeg ikke sætter returtypen til Dictionary
Lad os se, hvad det giver os, når vi skal udbygge GeneralLedgerBehaviour:
using System;
using NUnit.Framework;
using NMock2;[TestFixture()]
public class GeneralLedgerBehaviour
{private Mockery _mockery;
private IGeneralLedger _generalLedger;
private ILedgerAccount _ledgerAccount;[SetUp()]
public void setupBehaviour()
{_generalLedger = new GeneralLedger();
_mockery = new Mockery();
_ledgerAccount = _mockery.NewMock<ILedgerAccount>();}
#region [UserStory: Adding LedgerAccounts to GeneralLedger]
[Test()]
public void shouldAddLedgerAccountOnAddWithValidLedgerAccount()
{Expect.Once.On(_ledgerAccount).GetProperty(”IsValid”).Will(Return.Value(true));
Expect.AtLeastOnce.On(_ledgerAccount).GetProperty(”AccountId”).Will(Return.Value(10));
_generalLedger.AddAccount(_ledgerAccount);
Assert.AreEqual(1,_generalLedger.Accounts.Count);
Assert.IsTrue(_generalLedger.Accounts.Contains(10));}
[Test()]
[ExpectedException(typeof(ArgumentException))]
public void shouldThrowExceptionOnAddWithInvalidLedgerAccount()
{Expect.Once.On(_ledgerAccount).GetProperty(”IsValid”).Will(Return.Value(false));
_generalLedger.AddAccount(_ledgerAccount);}
[Test()]
[ExpectedException(typeof(ArgumentNullException))]
public void shouldThrowExceptionOnAddWithNullLedgerAccount()
{_ledgerAccount = null;
_generalLedger.AddAccount(_ledgerAccount);}
[Test()]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void shouldThrowExceptionWhenAddingLedgerAccountWithIdenticalId()
{Expect.Exactly(2).On(_ledgerAccount).GetProperty(”IsValid”).Will(Return.Value(true));
Expect.AtLeastOnce.On(_ledgerAccount).GetProperty(”AccountId”).Will(Return.Value(10));
_generalLedger.AddAccount(_ledgerAccount);
_generalLedger.AddAccount(_ledgerAccount);}
#endregion#region [UserStory: Deleting LedgerAccounts from GeneralLedger]
[Test()]
public void shouldDeleteSuppliedExistingLedgerAccount()
{Expect.Once.On(_ledgerAccount).GetProperty(”IsValid”).Will(Return.Value(true));
Expect.AtLeastOnce.On(_ledgerAccount).GetProperty(”AccountId”).Will(Return.Value(10));
_generalLedger.AddAccount(_ledgerAccount);
_generalLedger.DeleteAccount(_ledgerAccount);
Assert.IsFalse(_generalLedger.Accounts.Contains(_ledgerAccount.AccountId));}
[Test()]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void shouldThrowExceptionWhenDeletingNonExistingLedgerAccount()
{Expect.AtLeastOnce.On(_ledgerAccount).GetProperty(”AccountId”).Will(Return.Value(10));
_generalLedger.DeleteAccount(_ledgerAccount);}
[Test()]
[ExpectedException(typeof(ArgumentNullException))]
public void shouldThrowExceptionWhenDeletingNullLedgerAccount()
{_ledgerAccount = null;
_generalLedger.DeleteAccount(_ledgerAccount);}
#endregion}
Meget let og elegant, hvis jeg selv skal sige det. Den eneste forudsætning for at ovenstående vil kompilere er, at jeg laver en klasse som hedder GeneralLedger, som implementerer IGeneralLedger. Hvis jeg på et tidspunkt finder på at lave TheNewAndImprovedGeneralLedger, skal jeg kun skifte en enkelt reference i SetUp-metoden for at teste om den overholder mine specifikationer. I min næste post vil jeg vise en implementation, som overholder min specifikation.