Enhetsprovning Succintly Strategier för enhetstester

Testmetoderna beror på var du befinner dig i projektet och din budget, när det gäller tid, pengar, arbetskraft, behov etc. Idealiskt sett är enhetsprovning budgeterad i utvecklingsprocessen, men realistiskt möter vi ofta befintliga eller äldre program som har liten eller ingen koddekning men måste uppgraderas eller underhållas. 

Det värsta scenariot är en produkt som för närvarande utvecklas men uppvisar ett ökat antal misslyckanden under dess utveckling, igen med liten eller ingen koddekning. Som produktchef, antingen i början av en utvecklingsinsats eller som en följd av att man lämnat en befintlig ansökan, är det viktigt att utveckla en rimlig enhetsteststrategi. 

Kom ihåg att enhetsprov ska ge mätbara fördelar för ditt projekt för att kompensera ansvaret för deras utveckling, underhåll och egen testning. Dessutom kan strategin som du antar för din testning av enheten, påverka din applikations arkitektur. Även om det här nästan alltid är bra, kan det leda till onödiga kostnader för dina behov.

Från och med kraven

Om du börjar en tillräckligt komplicerad applikation från en ren skiffer, och allt som finns i dina händer är en uppsättning krav, överväga följande vägledning.

Prioritering av beräkningskrav

Prioritera applikationens beräkningskrav för att bestämma var komplexiteten ligger. Komplexiteten kan bestämmas genom att upptäcka antalet tillstånd som en viss beräkning måste rymma, eller det kan vara resultatet av en stor uppsättning av inmatningsdata som krävs för att utföra beräkningen, eller det kan helt enkelt vara algoritmiskt komplicerat, såsom att göra fallfallsanalys på en satellits redundansring. Tänk också på var koden sannolikt kommer att förändras i framtiden till följd av okända ändringar. Medan det låter som att det kräver klirvoyans, kan en skicklig programvaruarkitekt kategorisera koden i allmänt syfte (lösa ett vanligt problem) och domänspecifik (lösa ett specifikt kravproblem). Den senare blir en kandidat för framtida förändringar.

Samtidigt som man skriver enhetstester för triviala funktioner är lätt, snabbt och glädjande i antalet testfall som programmet slår igenom, de är de minst kostnadseffektiva testerna, de tar tid att skriva och eftersom de troligtvis kommer att skrivas korrekt till att börja med och de kommer sannolikt inte att förändras över tid, de är minst användbara eftersom programmets kodbas växer. I stället fokusera din testningsstrategi på koden som är domänspecifik och komplex.

Välj en arkitektur

En av fördelarna med att starta ett projekt från en uppsättning krav är att du får skapa arkitekturen (eller välj en tredje parts arkitektur) som en del av utvecklingsprocessen. Tredjepartsramar som gör det möjligt för dig att utnyttja arkitekturer som inversion av kontroll (och det relaterade begreppet dependence injection) samt formella arkitekturer som Model View View Controller (MVC) och Model View ViewModel (MVVM) underlättar enhetstestning av den enkla anledningen att en modulär arkitektur är vanligtvis enklare att enhetstest. Dessa arkitekturer skiljer sig ut:

  • Presentationen (visa).
  • Modellen (ansvarig för uthållighet och data representation).
  • Kontrollern (där beräkningarna ska ske).

Även om vissa aspekter av modellen kan vara kandidater för enhetstestning, kommer de flesta av enhetstesterna sannolikt att skrivas mot metoder i kontrollern eller visningsmodellen, där beräkningarna på modellen eller vyn implementeras.

Underhållsfas

Enhetstestning kan vara till nytta även om du är inblandad i underhållet av en applikation, en som antingen kräver att du lägger till nya funktioner i en befintlig applikation eller helt enkelt fixar buggar av ett äldre program. Det finns flera tillvägagångssätt som man kan ta till en befintlig applikation och frågor som ligger till grund för dessa metoder som kan avgöra om enhetsprovningens kostnadseffektivitet är:

  • Skrivar du enhetstester bara för nya funktioner och buggfixar? Är funktionen eller buggfixningen något som kommer att gynnas av regressionstestning, eller är det en enstaka, isolerad fråga som är lättare testad vid integreringstestning??
  • Börjar du skriva enhetsprov mot befintliga funktioner? Om så är fallet, hur prioriterar du vilka funktioner som ska testas först?
  • Fungerar den befintliga kodbasen bra med enhetstestning eller behöver koden först refactoring för att isolera kodenheter?
  • Vilka inställningar eller avbrott behövs för funktionen eller felprovningen?
  • Vilka beroenden kan upptäckas om kodändringar som kan leda till biverkningar i annan kod, och om enhetstesterna ska utökas för att testa uppförandet av beroende kod?

Att gå in i underhållsfasen av ett äldre program som saknar enhetstestning är inte trivialt - planeringen, övervägande och undersökning av koden kan ofta kräva mer resurser än att bara fixa buggen. Den goda användningen av enhetsprovning kan dock vara kostnadseffektiv och medan det inte alltid är lätt att bestämma är det värt träningen, om det inte finns någon annan anledning än att få en djupare förståelse av kodbasen.


Bestäm din process

Det finns tre strategier man kan ta med avseende på testprocessen: "Test-Driven Development", "Code First" och, även om det kan verka antithetiskt mot temat för denna bok, "No Unit Test" -processen.

Testdriven utveckling

Ett läger är "Testdriven utveckling", sammanfattad av följande arbetsflöde:

Med tanke på ett beräkningskrav (se tidigare avsnitt), skriv först en stub för metoden.

  • Om beroenden på andra objekt som ännu inte har implementerats krävs (objekt som skickas in som parametrar till metoden eller returneras av metoden), implementera dem som tomma gränssnitt.
  • Om egenskaper saknas, implementera stubbar för egenskaper som behövs för att verifiera resultaten.
  • Skriv några krav på inställning eller avbrott.
  • Skriv testen. Skälen till att skriva några stubbar innan skriva testet är: för det första att dra nytta av IntelliSense när du skriver provet; För det andra att fastställa att koden fortfarande sammanställs. och för det tredje för att säkerställa att metoden testas har dess parametrar, gränssnitt och egenskaper synkroniserats med avseende på namngivning.
  • Kör testen, verifiera att de misslyckas.
  • Kod genomförandet.
  • Kör testen, verifiera att de lyckas.

I praktiken är detta svårare än det ser ut. Det är lätt att falla byte för att skriva tester som inte är kostnadseffektiva, och ofta upptäcker man att metoden som testas inte är en tillräckligt fin kornad enhet för att faktiskt vara en bra kandidat för ett test. Kanske gör metoden för mycket, kräver för mycket inställning eller avbrott, eller har beroenden på alltför många andra objekt som alla måste initieras till ett känt tillstånd. Det här är allt som lättare upptäcks när du skriver koden, inte testet.

En fördel med ett testdriven tillvägagångssätt är att processen inleder disciplinen för enhetstestning och skrivning av enhetstesterna först. Det är lätt att avgöra om utvecklaren följer processen. Med övning kan man bli enkel att göra processen också kostnadseffektiv.

En annan fördel med ett testdriven tillvägagångssätt är att det i sin natur styrker en slags arkitektur. Det skulle vara absurt men möjligt att skriva ett enhetstest som initierar en form, sätter värden i en kontroll och ringer sedan en metod som förväntas utföra en viss beräkning på värdena, vilket den här koden skulle kräva (finns faktiskt här):

privat tomt btnCalculate_Click (objekt avsändare, System.EventArgs e) double Principal, AnnualRate, InterestEarned; dubbel FutureValue, RatePerPeriod; int NumberOfPeriods, CompoundType; Principal = Double.Parse (txtPrincipal.Text); AnnualRate = Double.Parse (txtInterest.Text) / 100; om (rdoMonthly.Checked) CompoundType = 12; annars om (rdoQuarterly.Checked) CompoundType = 4; annars om (rdoSemiannually.Checked) CompoundType = 2; annars CompoundType = 1; NumberOfPeriods = Int32.Parse (txtPeriods.Text); dubbel i = AnnualRate / CompoundType; int n = CompoundType * NumberOfPeriods; RatePerPeriod = AnnualRate / NumberOfPeriods; FutureValue = Principal * Math.Pow (1 + i, n); InterestEarned = FutureValue - Principal; txtInterestEarned.Text = InterestEarned.ToString ("C"); txtAmountEarned.Text = FutureValue.ToString ("C"); 

Den föregående koden är otestbar eftersom den är inkopplad med händelsehanteraren och användargränssnittet. Snarare kan man skriva beräkningsmetoden för sammansatt ränta:

public enum CompoundType Årligen = 1, SemiAnnually = 2, Kvartals = 4, Månadlig = 12 Privat dubbel CompoundInterestCalculation (double principal, double annualRate, CompoundType compoundType, int perioder) double annualRateDecimal = annualRate / 100.0; double i = annualRateDecimal / (int) compoundType; int n = (int) compoundType * perioder; double ratePerPeriod = annualRateDecimal / periods; dubbel futureValue = principal * Math.Pow (1 + i, n); double interestEaned = futureValue - principal; returnera intresseEaned; 

vilket skulle göra det möjligt för ett enkelt test att skrivas:

[TestMethod] public void CompoundInterestTest () dubbel intresse = CompoundInterestCalculation (2500, 7.55, CompoundType.Monthly, 4); Assert.AreEqual (878,21, intresse, 0,01); 

Vidare, genom att använda parametrerad testning, skulle det vara enkelt att testa varje föreningstyp, ett antal år och olika räntor och huvudbelopp.

Det testdrivna tillvägagångssättet faktiskt underlättar en mer formaliserad utvecklingsprocess genom att upptäcka faktiska testbara enheter och isolera dem från gränsöverskridande beroenden.

Kod först, test andra

Kodning först är mer naturligt om bara för det är det vanliga sättet applikationer utvecklas. Kravet och dess genomförande kan också tyckas lätt nog vid första ögonkastet, så att skriva flera enhetstester verkar som en dålig tidsanvändning. Andra faktorer som tidsfrister kan tvinga ett projekt till en "bara få koden skriven så att vi kan skicka" utvecklingsprocessen.

Problemet med kodens första tillvägagångssätt är att det är lätt att skriva kod som kräver den typ av test vi såg tidigare. Kod först kräver en aktiv disciplin att testa koden som har skrivits. Denna disciplin är oerhört svår att uppnå, särskilt eftersom det alltid finns nästa nya funktion att genomföra.

Det kräver också intelligens, om du vill, för att undvika att skriva inhopad, gränsövergångskod och disciplinen att göra det. Vem har inte klickat på en knapp i Visual Studio Designer och kodade händelsens beräkning direkt i stuben som Visual Studio skapar för dig? Det är enkelt och eftersom verktyget riktar dig i den riktningen kommer den naiva programmeraren att tro att det här är rätt sätt att koda.

Detta tillvägagångssätt kräver noggrant övervägande av teamets färdigheter och disciplin, och kräver närmare övervakning av laget, särskilt under högspänningsperioder när disciplinerade tillvägagångssätt tenderar att bryta ner. Beviljas, en testdriven disciplin kan också kastas ut som deadlines, men det brukar vara ett medvetet beslut att göra ett undantag, medan det lätt kan bli en regel i en kodens första tillvägagångssätt.

Inga enhetstester

Bara för att du inte har enhetstest betyder inte att du slänger ut testning. Det kan helt enkelt vara att testningen betonar acceptansprovningsprocedurer eller integrationstestning.

Balanseringsteststrategier

En kostnadseffektiv testprocess kräver en balans mellan testdriven utveckling, kod först, test andra och "test några andra sätt" -strategier. Kostnadseffektiviteten hos enhetstestning bör alltid övervägas, liksom faktorer som utvecklarens erfarenhet av laget. Som chef kanske du inte vill höra att en testdriven strategi är en bra ide om ditt lag är ganska grönt och du behöver processen för att införa disciplin och tillvägagångssätt.