mandag 6. juli 2009 NDC TDD C#
Av alle de spennende tingene som skulle skje på Norwegian Developer Conference 2009, var det en ting jeg gledet meg mere til enn noe annet. Jeg skulle nemlig få oppleve en hel dags workshop med Scott Bellware.
http://www.flickr.com/photos/digidragon/ / CC BY-NC-ND 2.0
Bellware er en utvikler og Agile coach fra Austin, Texas. Han har fått MVP-tittelen fra Microsoft fem ganger, men er mest kjent for å være en sentral person i Alt.Net miljøet, og kritiserer Microsoft ganske åpenlyst. Han er en fargerik person, og kan karrakteriseres som en aldri så liten bråkebøtte. Følger du @bellware på twitter så merker du raskt hva jeg mener.
Jeg var så heldig å få "henge litt" med Scott utenom workshopen og foredragene også, og selv om han er Stor i Kjeften har han mye interessant å komme med, og jeg synes det er helt fantastisk at Jon Arild har invitert Scott til å komme å forelese for NNUG i Bergen senere i år.
Workshop'en
Workshopen handlet i hovedsak om å skrive automatiserte tester i form av Context Specifictions. Scott's budskap var at vi må gjemme all støyen fra testene/spesifikasjonene våre - testene er dokumentasjon, og skal være enkle å lese. Han sier at man oppnår høy produktivitet i utvikling om man har et godt design, og et godt design kommer av god TDD.
Med dette som bakteppe kom Bellware inn på mange ulike områder, og her er noen høydepunkter fra notatene mine:
TDD: De to første stegene i TDD - (1) lag en test som feiler og (2) få den til å passere på enklest mulig måte - er egentlig en test av selve testen. Ved å bevise at den først feiler og deretter passerer ved forventet "input til asserten" kan man stole på testen. Først etter at testen er grønn implementerer man funksjonaliteten slik den skal være. (Jeg har forsøkt å poengtere det samme flere ganger, men er ikke alltid like flink til å følge det selv.)
TDD: Gjør TDD i så små steg som mulig. "Jeg trodde at de små stegene Kent Beck presenterer i TDD boken sin var som de var for å illustrere prinsippene," sa Scott. "Det tok meg to år å skjønne at det faktisk er sånn de beste utviklerne praktiserer TDD."
TDD: Jobb bare med én test om gangen. Gjør du en endring som fører til at to tester må endres så gjør du sansynlig vis for mye på en gang.
TDD: Ikke bryt testene dine. Skal du gjøre en refakturering som krever en endring av testen er det bedre å legge tilrette for å endre testen uten å fjerne den eksisterende implementasjonen. Når alt er på plass kan du endre testen, uten at den behøvde å bli rød på noe som helst tidspunkt. Til slutt kan du fjerne den delen av implementasjonen som støttet den gamle versjonen av testen.
TDD: Husk å alltid skrive hva du ønsker skal eksistere før du implementerer det. Ikke bar når du skriver tester. "Jeg trenger en metode" oppdages ved å kalle metoden (som ikke finnes enda). Først deretter implementeres den.
DDD og IoC: Unngå dependency injection inn i metoder
DDD og IoC: Unngå dependency injection inn i Enteties
DDD: Enteties bør ikke tilby aggregert data (rapportdata) - som f.eks. TotalOfPreviousOrders
DDD: customer.Orders er no no! repository.GetOrders(customer) er bra!
Verktøy: Check in til kildekode-systemet hele tiden! Hver gang du har gjort en endring som fungerer: Get latest, run the tests (nant/rake), and commit changes.
Verktøy: Autohotkey er et interresant verktøy. Scott gav oss et autohotkey script som lar deg bytte ut space med underscore og dermed gjør den enklere å skrive metode_og_klassenavn_som_dette.
Verktøy: Sett opp shortcut for å kjøre en eller flere tester med TestDriver.net og en annen shortcut for å kjøre tester om igjen. Dermed går det raskere å kjøre samme test eller testsett flere ganger mens man jobber i koden.
Scott brukte også endel tid på å snakke om det jeg kjenner som the onion architecture. Han beskrev dette som et grunnleggende Alt.Net pattern som brukes over alt for tiden. Det virker veldig logisk og i trå med alt jeg har lest av Uncle Bob i det siste, og jeg må sette meg grundig inn i dette og finne ut hvordan det vil endre hvordan jeg designer løsningene mine.
En av testene vi skrev..
Her er et eksempel på hva vi produserte i workshopen. Denne spesifikasjonen beviser at en ordre tar høyde for avslag når man ber om prisen. Scott brukte MSpec, Machine Specification, til å skrive testene.
9 [Subject("Order")]
10 public class when_discounting_an_order
11 : OrderContext
12 {
13 Establish context = () =>
14 {
15 order = order_with_total_of(100m);
16 order.DiscountPercentage = 10m;
17 order_total = order.GetTotal();
18 };
19
20 It should_discount_the_order_by_10_percent = () =>
21 {
22 order_total.ShouldEqual(90m);
23 };
24 }
Spesifikasjonen er enkel å forstå, den lar seg lese nesten som vanlig engelsk. Når man har en ordre med en verdi av 100 dollar, og man gir 10% avslag, så skal prisen reduseres med 10%, hvilket vil si at totalen skal være 90 dollar.
Detaljene er gravd ned i OrderContext, som spesifikasjonen arver fra:
26 public class OrderContext
27 {
28 public static Order order_with_total_of(decimal amount)
29 {
30 var newOrder = new Order(new Customer());
31 var item = new Item(amount);
32 newOrder.AddItem(item);
33 return newOrder;
34 }
35
36 protected static decimal order_total;
37 protected static Order order;
38 }
Jeg har tidligere blogget om hvordan jeg har gått fra å skrive tradisjonelle enhetstester til å skrive GivenWhenThen senarioer med et rammeverk som heter TinyBDD. Bellware gav meg mange flere tips om hvordan slike spesifikasjoner bør konstrueres. Det ironiske er at jeg etter dette har droppet å bruke BDD-rammeverk til fordel for å skrive mine egne DSL'er. TinyBDD, MSpec og lignende skaper mer støy enn nødvendig, og ved å skrive ren og selvdokumenterende kode føler jeg at jeg kan oppnå vel så god effekt uten dem.
Her er et eksempel på hvordan testene jeg skriver for tiden ser ut:
1 [TestFixture]
2 public class ConsoleGameIntroViewTest : ConsoleTestingFixture
3 {
4 [Test]
5 public void Should_display_introduction_text()
6 {
7 Given_a_game();
8 With_introduction(This_is_an_introduction);
9 When_displaying_view();
10 Output.should_contain(This_is_an_introduction);
11 }
12
13 [Test]
14 public void Should_display_title()
15 {
16 Given_a_game();
17 With_title(A_Long_Time_Ago_in_a_Galaxy_Far_Far_Away);
18 When_displaying_view();
19 Output.should_contain(A_Long_Time_Ago_in_a_Galaxy_Far_Far_Away);
20 }
21
22 Action Given_a_game = () => gameLevel = new GameLevel();
23
24 Action<string> With_introduction = (introduction) =>
25 gameLevel.IntroductionText = introduction;
26
27 Action<string> With_title = (title) =>
28 gameLevel.Title = title;
29
30 Action When_displaying_view = () =>
31 {
32 introView = new ConsoleGameIntroView();
33 introView.Show(gameLevel);
34 };
35
36 static GameLevel gameLevel;
37 static ConsoleGameIntroView introView;
38 const string This_is_an_introduction = "This is an introduction";
39 const string A_Long_Time_Ago_in_a_Galaxy_Far_Far_Away = "A Long Time Ago in a Galaxy Far Far Away";
40 }
Dette er en testklasse for et View i et konsoll-basert spill som bruker Model-View-Presenter. Det viktigste kommer først - selve testene. Og de kan leses av hvem som helst, og lar deg fokusere på adferden - implementasjonsdetaljene er støy, så de kommer lengre nede.
Du synes kanskje dette er galskap? Jeg føler denne måten å spesifisere adferd på hjelper meg å skrive bedre kode, og de dokumenterer ganske bra hva systemet skal gjøre. Forvent mer om mine enhetstester i kommende blogposter..
Mer Bellware?
Er du interessert i å høre mer om hva Bellware har å si om TDD/BDD anbefaler jeg at du lytter til Hanselminutes #146: Test Driven Development is Design - The Last Word on TDD.
Og er du mer interessert i hans bråkete side kan du lytte til Alt.Net podcast #17: The State of Alt.Net, hvor han rakker ned på miljøet han har vært med å bygge opp. I så fall bør du også laste ned show nummer 18: Talking with Jeremy Miller about Alt.Net, som forsøker å roe ned situasjonen igjen.
Knagger: Alt.Net, BDD, Bellware, DDD, Dependency injection, NDC, NNUG, TinyBDD