Enhetstesting av konsoll-applikasjoner

I artikkelen min on Bellware’s NDC workshop inkluderte jeg en enhetstest jeg hadde skrevet for å teste output til System.Console. Her følger jeg opp med å forklare hvordan jeg gjorde det.

I Snookiepoof, hvor testen var hentet fra, ønsker jeg å ha så høy test coverage som mulig. Jeg utviklet nesten alt vha TDD/BDD, men brukegrensesnittet – et tynt lag som presenterte ting til konsollet – hadde jeg ingen tester for. Etterhvert begynte dette å irritere meg, og jeg måtte finne en måte å få dette under test på.

Her er et eksempel på en test jeg ønsket å kjøre:

    1 [Test]

    2 public void Should_output_new_value()

    3 {

    4     Given_a_ConsoleCreditChangedView();

    5     When_credit_changes_to(42);

    6     Output.should_contain(“42″);

    7 }

Jeg har et ConsoleCreditChangedView, en konkret implementasjon av et abstrakte CreditChangedView, som skriver til konsollet. Når viewet blir bedt om å vise en CreditChange med en bestemt verdi ønsker jeg å sjekke at den bestemte verdien blir skrevet ut til konsollet (linje 6).

Her er resten av detaljene fra testen, men uten å avsløre hvordan jeg får tak i Output (litt tålmodighet):

    9     Action Given_a_ConsoleCreditChangedView = () =>

   10         consoleCreditChangedView = new ConsoleCreditChangedView();       

   11 

   12     Action<int> When_credit_changes_to = (creditValue) =>

   13         consoleCreditChangedView.Show(new CreditChangedEventArgs(creditValue));

   14 

   15     static ConsoleCreditChangedView consoleCreditChangedView;

   16 }

Et naturlig valg hadde vært å abstrahere bort System.Console, og i stedet skrive til et interface, som jeg så kunne mocke bort i enhetstestene. Men jeg hadde brukte Console.WriteLine mange steder, og det ante meg at det fantes en annen løsning.

En rask titt på msdn-dokumentasjonen viste meg at jeg kunne erstatte konsollets default input og output. Det var akkurat det jeg trengte. Under normal kjøring kunne jeg la konsollet være som det er, mens jeg i enhetstestene kunne bytte ut input og output med noe jeg kunne aksessere og manipulere i testkoden.

Etter å ha prøvet og feilet litt kom jeg opp med løsningen nedenfor; én klasse for å “lure ut” output, én tilsvarende klasse for input, og en test fixture som kombinerte de to.

ConsoleTestingFixture

La oss ta en titt på ConsoleOutputFaker først:

    1 internal class ConsoleOutputFaker

    2 {

    3     private StringWriter _stringWriter;

    4 

    5     internal void SwapConsoleOutput()

    6     {

    7         _stringWriter = new StringWriter();

    8         System.Console.SetOut(_stringWriter);

    9     }

   10 

   11     internal string GetOutput()

   12     {

   13         return _stringWriter.ToString();           

   14     }

   15 

   16     internal void SwapBack()

   17     {

   18         RevertBackToOriginalOutput();

   19         DisposeResources();

   20     }

   21 

   22     private void RevertBackToOriginalOutput()

   23     {

   24         var standardOut = new StreamWriter(System.Console.OpenStandardOutput());

   25         standardOut.AutoFlush = true;

   26         System.Console.SetOut(standardOut);

   27     }

   28 

   29     private void DisposeResources()

   30     {

   31         if (_stringWriter != null)

   32         {

   33             _stringWriter.Close();

   34             _stringWriter = null;

   35         }           

   36     }

   37 }

I linje 7 og 8 kan du se hvordan jeg erstatter konsollets normale output med en StringWriter. Når programkoden min så skriver til konsollet så skrives det egentlig til denne StringWriter’en, og jeg kan hente ut og verifisere inneholdet (linje 13).

Resten av klasser er “cleanup” – linje 24 til 26 viser hvordan output settes tilbake til normalen igjen.

Som lovet lagde jeg også en input faker:

    1 internal class ConsoleInputFaker

    2 {

    3     private StringReader _inputReader;

    4 

    5     internal void SendInput(string text)

    6     {

    7         _inputReader = new StringReader(text);

    8         System.Console.SetIn(_inputReader);           

    9     }

   10 

   11     internal void SwapBack()

   12     {

   13         RevertBackToOriginalInput();

   14         DisposeResources();

   15     }

   16 

   17     private static void RevertBackToOriginalInput()

   18     {

   19         var originalInput = System.Console.OpenStandardInput();

   20         var originalReader = new StreamReader(originalInput);

   21         System.Console.SetIn(originalReader);

   22     }

   23 

   24     private void DisposeResources()

   25     {

   26         if (_inputReader != null)

   27         {

   28             _inputReader.Close();

   29             _inputReader = null;

   30         }

   31     }

   32 }

Som du ser er prinsippet det samme. Jeg setter konsollets input til en ny StringReader (linje 7 og 8). StringReader’en inneholder alerede teksten jeg ønsker å sende til konsollet, så nå kan jeg simulere brukerinteraksjon.

Som jeg sa kombinerer jeg så de to klassene i en test fixture:

    1 public class ConsoleTestingFixture

    2 {

    3     private static ConsoleOutputFaker fakeOut;

    4     private static ConsoleInputFaker fakeIn;

    5 

    6     static ConsoleTestingFixture()

    7     {

    8         fakeOut = new ConsoleOutputFaker();

    9         fakeIn = new ConsoleInputFaker();

   10     }

   11 

   12     [SetUp]

   13     public void TestSetUp()

   14     {

   15         fakeOut.SwapConsoleOutput();

   16     }

   17 

   18     protected static string Output

   19     {

   20         get

   21         {

   22             return fakeOut.GetOutput();

   23         }

   24     }

   25 

   26     protected static void SendInput(string text)

   27     {

   28         fakeIn.SendInput(text);

   29     }

   30 

   31     [TearDown]

   32     public void TestTearDown()

   33     {

   34         fakeOut.SwapBack();

   35         fakeIn.SwapBack();

   36     }

   37 }

Fixturen har en SetUp metode og en TearDown metode: Før hver enhetstest erstattes standard output med min fake, og etter hver enhetstest settes normal output og input tilbake til default.

Fixturen tilbyr så en property for å få tilgang til output (linje 18), og en metode for å sende input (linje 26). Test-klasser som arver fra denne fixturen kan bruke SendInput til å simulere at en bruker skriver noe til konsollet, og aksessere Output for å kontrollere hva programmet skriver til brukeren.

Du lurer kanskje på hvorfor jeg har brukt så mye static i test fixturen? Det kommer av måten jeg liker å skrive testklassene mine på for tiden – hvor jeg bruker Actions deklarert som instans-variabler – og da må det de skal ha tilgang til være static.

Her er et eksempel på en test som simulerer input. Denne klassen arver fra ConsoleTestingFixture, og får dermed tilgang til SendInput-metoden.

    1 [Test]

    2 public void Should_be_able_to_go_back_to_game_from_summary()

    3 {

    4     Given_a_known_ViewModel();

    5     When_the_summary_is_shown();

    6     And_the_user_types(back);

    7     The_user_selection_should_be(GameSummaryResult.ResumeCurrentGame);

    8 }

Og detaljene for And_the_user_types:

    1 Action<string> And_the_user_types = (input) =>

    2     SendInput(input);      

Så hvis du føler deg litt retro en dag og får lyst til å lage en konsoll app, så har du nå ingen unnskydning for å ikke skrive tester :)

Her er en annen blogpost om å erstatte Console.Out og Console.In som jeg brukte som referanse når jeg implementerte min fixture. Og her er en fyr som har gått for en løsning med attributter på test-metodene for å spesifisere input og forventet output.

Det kan også være lurt å ta en titt på hva test-rammeverket du bruker har for støtte av konsoll-testing “out of the box”. Skriver du mye konsoll-applikasjoner bør du kanskje også se på NConsoler, et open source biblotek for å bygge konsoll apps som også skal ha noe støtte for enhetstester (sies det).

Lurer du forresten på hvor should_contain metoden jeg kalte på Output i linje 6 i den aller første testen kommer fra? For å skrive lesbare tester er det ganske vanlig å erstatte de vanlige kallene til Assert med metodenavn som flyter bedre. Klikk her for å ta en titt på min FluentAsserts.cs som oversetter NUnits mange asserts til noe jeg er mere fornøyd med vha Extension methods. Koden er mer eller mindre rappet fra open source prosjektet coretdd, som også inneholder tilsvarene metoder for xUnit, MbUnit og MsTest.

Knagger: , , ,

Kategorier: Testing / TDD.
RSS feed for kommentarene. Tilbaketråkk.

2 kommentarer til “Enhetstesting av konsoll-applikasjoner”

  1. Halvard Says:

    Jeg leste et sted nylig at aa starte med en konsollapplikasjon for deretter aa skrive den om til en vanlig windows eller WPF eller hva det enn skulle vaere, at det resulterer i bedre kode. Da blir man tvunget til aa refaktorere riktig for aa komme bort bra duplikat kode (forutsatt DRY-prinsippet) i viewet og at man ender opp med et API som er godt tilpasset aa slippe til eksterne parter. I farten fant jeg ikke linken, men med din mock her vil det jo vaere enkelt aa teste i hvert fall. Om det er noe i utsagnet har jeg heller ingen formening om.

  2. Torbjørn Says:

    Jeg mener vi som regel bør skrive applikasjonene våre helt uavhengig av hvilket type brukergrensesnitt man ønsker å benytte. Brorparten av programmene våre bør være i form av moduler som kan benyttes sammen med et hvilket som helst grensesnitt. Konsollet er kanskje det grensesnittet som skaper minst “støy” – hvis man starter med WPF, WebForms el.l., så vil rammeverket lett kunne påvirke løsningen din, så det er nok noe i påstanden du refererer til. Selv vil jeg anbefale å starte med et enkelt Class Library prosjekt, og så lar du testene være brukergrensesnittet ditt.

    Og så legger du et så tynt brukergrensesnitt som mulig oppå toppen når du trenger å vise noe til kunden.

Skriv en kommentar

Tillatte tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Torbjørn: La oss anta to ulike definisjoner av Template Method pattern - de to ytterpunkte...

Lars-Petter: Hei igjen. Siden du inviterer til å ta diskusjonen i bloggen, og har tatt deg t...

Torbjørn: Lars-Petter: Det er én måte å se det på. Det er absolutt fortsatt Template M...

Lars-Petter: Hei. Har du ikke i prinsippet her gått over fra Template Method (arv) til Strat...

Christian Abildsø: I alle fall i C#, så føles dette som kode som blir mer fleksibel men vanskelig...

Torbjørn: Hei Henrik, og takk for tilbudet. Ble oppmerksom på Rasberry Pi for under en uk...

Henrik Sandaker Palm: Ang. større hobby prosjekt. Du er som er en slik rakker på programmering har j...

Øivind Nilsen: Slutt å bruke mobilnummeret mitt som eksempel !...

Bjørn Einar Bjartnes: Jeg har også latt meg fascinere av Clojure, uten at jeg har kommet så veldig l...

Bjørn Einar Bjartnes: Sweet :) Jeg tror egentlig jeg liker det som det er, med musikk. Litt av utford...

Mulig relaterte linker

 Hold deg oppdatert

Søk i bloggen

Ferske innlegg

  • Template Method del 4: Multippel arv
  • Template Method Intermesso
  • Template Method del 3: Bare funksjoner
  • Template Method del 2: På vei mot funksjonell programmering
  • Kategorier

  • .net ninja (37)
  • Bøker (17)
  • Diverse prosjekter (35)
  • DSL (10)
  • Erlang (10)
  • F# (5)
  • Hardware (1)
  • Jobb (77)
  • Julekalender (51)
  • kjempekjekt.com (23)
  • LISP/Clojure (33)
  • NNUG / community (60)
  • O/RM & databaser (10)
  • Off topic (116)
  • OO-design/clean code (30)
  • Podcasts (14)
  • Polyglot (77)
  • Ruby (27)
  • Silverlight / RIA (3)
  • Software/verktøy (20)
  • Softwareutvikling (20)
  • Testing / TDD (30)
  • the contiki strip (13)
  • User experience (3)
  • WCF (3)
  • Webutvikling (32)
  • WPF (9)
  • WTF (12)
  • Last ned Wallpaper

    Programmeringsbloggens tøffe skrivebordsbakgrunn med snippets fra ulike språk laster du ned her!

    Abonner via epost

    Om du vil kan du få alle nye blogposter tilsendt til din epost. Abonner nå, det er kjempeenkelt!

    Meta