Det er endelig lykkedes mig at finde et lille projekt, som jeg kan køre strengt efter TDD metodikken, med andre ord testen skrives først. I den forbindelse er der noget mere fokus på access modifiers end når vægten er på integrationstests, men man må jo hænge i og gøre plads til injektere fake objekter efter bogen.

Mens jeg er godt i gang med implementering af validering i et business objekt støder jeg så ind i en "hvorfor-nu-det" situation, som stadig ikke er helt klar for mig. Vi har følgende setup eksempliceret ved det klassiske books eksempel.

I klassen "BookBusinessObject" ønsker vi at teste om en given bog (CurrentBook) findes i en liste af bøger (ListOfBooks). Listen kan fx initialiseres via opslag i en database men for at lette presset lidt på databasen gemmes listen i en statisk liste (books). Den unge dansker skriver sin unit test og får en fin grøn lampe fra NUnit med følgende kode;

    1 using System.Collections.Generic;

    2 using NUnit.Framework;

    3 using Rhino.Mocks;

    4 

    5 public class Book

    6 {

    7     public string Author { get; set; }

    8     public string Title { get; set; }

    9 }

   10 

   11 public class BookBusinessObject

   12 {       

   13     private static List<Book> books;

   14 

   15     public Book CurrentBook { get; set; }

   16 

   17     public virtual List<Book> ListOfBooks()

   18     {

   19         if (books == null)

   20         {

   21             InitializeBooks();

   22         }

   23 

   24         return books;

   25     }

   26 

   27     private static void InitializeBooks()

   28     {

   29         List<Book> bookList = new List<Book>();

   30         bookList.Add(new Book { Title = "Microsoft .NET: The Programming Bible", Author = "O'Brien, Tim" });

   31         bookList.Add(new Book { Title = "XML Developer's Guide", Author = "Gambardella, Matthew" });

   32 

   33         books = bookList;

   34     }

   35 

   36     public bool IsBookInList()

   37     {

   38         foreach (Book b in ListOfBooks())

   39         {

   40             System.Console.WriteLine(b.Title);

   41         }

   42 

   43         return this.ListOfBooks().Contains(this.CurrentBook);

   44     }

   45 }

   46 

   47 [TestFixture]

   48 public class BookBusinessObjectTest

   49 {

   50     [Test]

   51     public void IsBookInList_BookInList_ReturnTrue()

   52     {

   53         // arrange

   54         List<Book> fakeBookList = new List<Book>();

   55         Book fakebook = new Book { Title = "TDD for dummies", Author = "John Doe" };

   56         fakeBookList.Add(fakebook);

   57 

   58         BookBusinessObject businessObjectMock = MockRepository.GenerateStub<BookBusinessObject>();

   59         businessObjectMock.Stub(x => x.ListOfBooks()).Return(fakeBookList);

   60 

   61         businessObjectMock.CurrentBook = fakebook;

   62 

   63         // act

   64         bool included = businessObjectMock.IsBookInList();

   65 

   66         // assert

   67         Assert.IsTrue(included);

   68     }

   69 }

Der hviles et par minutter på laurbærerne inden næste test skrives. Det er ikke nok at vide om en given bog findes i listen, der skal også tilføjes andre valideringsregler så der kan med fordel oprettes en IsValid() metode, der kalder alle business objektets valideringsregler og giver en samlet vurdering af om business objektet er valid.

I unit testen for IsValid() kan det være en fordel at vi i stedet for at lave en fakeBookList blot laver et fake metodekald til IsBookInList() der altid returnerer true. Dermed skal der ikke sættes en masse op for at test IsValid(), hvis vi antager at IsBookInList() blot er én af mange ting, der skal være opfyldt.

Så for at Rhino Mocks kan fake retursvaret fra IsBookInList() ændrer vi accessoren i IsBookInList() fra "public" til "public virtual", men hov!! Det giver problemer, for nu siger NUnit;

TestCase 'BookBusinessObjectTest.IsBookInList_BookInList_ReturnTrue' failed:
  Expected: True
  But was:  False

0 passed, 1 failed, 0 skipped, took 2,64 seconds (NUnit 2.4).

Og det store spørgsmål er; hvorfor nu det?!?

Hjælpelinjerne 38-41 angiver, at fakeBookList ikke bliver injekteret da ListOfBooks() er tom. Ændres accessoren i stedet til "internal virtual" så er testen igen OK. Hvor er det at filmen knækker? Og hvad er best practice?

Kommentarer

Mark Seemann Denmark siger:

16. oktober 2009 15:41

Per default gør de fleste dynamick mock det, at når du danner en mock eller en stub, så overrider de alle public virtual (inkl. abstract) members, også dem, hvor du ikke har sat en eksplicit expectation. Det er det, der sker i din test: Så længe IsBookInList er non-virtual, benyttes den givne implementering (ander er ikke muligt).

Når metoden bliver virtual, bliver den automatisk overridden, og når du ikke har sat en expectation returneres default-værdien af returtypen - dvs. false for bools.

Når du ændrer accessoren til internal virtual har du effektivt set gjort den internal, og RhinoMocks kan ikke override den, da den ikke har adgang til internals (Rhino Mocks bor i et andet assembly end BookBusinessObject).

Jeg kan ikke huske hvad RhinoMocks gør, men Moq har en property kaldet CallBase, hvor man kan bede den om at benytte base-implementeringen når der ikke er eksplicitte setups. RhinoMocks har muligvis noget af det samme.

Grundlæggende er det dog altid en lille smule besværligt at TDD'e når der er virtual mebers til stede. Det er lidt lettere når der er tale om pure virtual members (interfaces eller abstract members), så det bør være din default-tilgang.

I dit eksempel ville du f.eks. være bedre tjent med at benytte Dependency Injection (DI) til at injecte et BookRepository ind i dit domain object.

t4rzsan Denmark siger:

17. oktober 2009 09:44

Mark har fat i det rigtige.  Initialiseringen af boglisten bør laves med DI.  Det vil gøre, at du ikke skal mocke selve dit domain object, men i stedet dit DI interface.  Hvis du insisterer på, at din liste af bøger skal være static, kan du enten bruge Martin Fowler's Registry Pattern for din BookBusinessObject klasse, eller du kan lade DI objektet bestemme, om listen skal være static eller ej (hvilket jeg nok hælder til).

pandasan Denmark siger:

17. oktober 2009 13:34

Tak! Jeg vil prøve at anvende DI i stedet.

Jeg kan godt se at Rhino Mock gør mere end jeg troede jeg bad den om - der blev kastet en del med exceptions, da jeg lukkede øjnene og bad den lave en stub af en Entity Framework entitet - i stedet for at anvende .CreateXX på XX klassen.

Rasmus Kromann-Larsen Denmark siger:

18. oktober 2009 10:35

Mark har jo allerede givet det gode svar og også nævnt DI og pure virtual members, så der er ikke så meget at tilføje ud over at jeg personligt (kan i hvert fald ikke huske hvornår jeg sidst har gjort det) helt undgår at mocke konkrete klasser og kun mocker interfaces. Jeg bliver altid ramt af et eller andet uventet når jeg mocker konkrete klasse :-)

Så interfaces kan helt sikkert anbefales.

Kommentarerne er lukkede