BLOGas.lt
Sukurk savo BLOGą Kitas atsitiktinis BLOGas

Modulių testų rekomendacijos – kodo aprėptis

Parašė Sergejus | 2011-08-16 18:51

Per paskutines dvi savaites teko rašyti ypatingai daug modulių testų (angl. unit tests) ir net pavyko pasiekti „išsvajotą“ 100% kodo aprėptį (angl. code coverage). Iš to gimė mintis parašyti kelių straipsnių ciklą apie rekomendacijas rašant modulių testus.

Modulių testų rekomendacijos – įrankiai ir aplinkos paruošimas
Modulių testų rekomendacijos – 13 modulių testų rašymo taisyklių
Modulių testų rekomendacijos – modulių testas ar integracijų testas?
Modulių testų rekomendacijos – kodo aprėptis

Praeitoje dalyje aš paaiškinau skirtumus tarp modulių testų ir integracijų testų bei išnagrinėjome integracijų testų rašymo taisykles. Šioje, paskutinėje, dalyje mes aptarsime svarbią testavimo metriką – kodo aprėptį.

Kas yra kodo aprėptis?

Kodo aprėptis nusako koks funkcionalumo procentas yra padengtas testais arba kitaip tariant, kokia kodo dalis yra vykdoma leidžiant testus. Natūraliai atrodo, kuo didesnis šis procentas, tuo labiau galima pasitikėti testais. Deja, praktikoje tai ne visada tiesa. Išnagrinėkime klasę StringHelper ir jos testą:

public static class StringHelper
{
  public static string Reverse(string input)
  {
    var arr = input.ToCharArray();
    Array.Reverse(arr);
    return new String(arr);
  }
}

[Test]
public void Given_Not_Empty_String_It_Is_Correctly_Reversed()
{
  // Arrange
  var text = "Test text";
  var reversedText = "txet tseT";

  // Act  
  var result = StringHelper.Reverse(text);

  // Assert
  Assert.AreEqual(reversedText, result);
}

Pateikto testo pilnai užtenka, kad pasiekti 100% StringHelper kodo aprėptį. 100%! Ar tai reiškia, kad mūsų kodas yra visiškai be klaidų? Nebūtinai! Nors testo metu techniškai ir vykdomas visas Reverse metodo kodas, mes nežinome kas įvyks, kai bus perduota null reikšmė. Ogi gausime NullReferenceException pačioje pirmoje Reverse eilutėje. Padarykime tokį tikrinimą:

public static class StringHelper
{
  public static string Reverse(string input)
  {
    if (input == null) throw ArgumentNullException("input");
    var arr = input.ToCharArray();
    Array.Reverse(arr);
    return new String(arr);
  }
}

Toks kodas yra teisingesnis, bet mes jau nebeturime 100% kodo aprėpties. Tam reikia antro testo:

[Test]
[ExpectedException(typeof(ArgumentNullException))]
public void Given_Null_Argument_Exception_Is_Thrown()
{
  // Arrange
  var nullArgument = null;

  // Act
  StringHelper.Reverse(nullArgument);
}

Ir vėl turime 100% aprėptį, ar dabar kodas neturi klaidų? Šį kartą nežinome, ar kodas gerai veiks tu tuščia eilute. Po kelių tokių iteracijų, aš pradėjau suprasti tikrąją testais grindžiamo programavimo (angl. Test Driven Development) naudą, bet čia visai kita tema…

Koks yra rekomenduojamas kodo aprėpties procentas?

Kaip visada, atsakymas į tokius klausimas yra – „priklauso“. Kuo daugiau jūsų metodas turi apsaugų nuo blogų reikšmių, tuo daugiau testų reikia pasiekti gerą kodo aprėptį. Aš prisilaikau tokios nykščio taisyklės – ne mažiau kaip 80%.

 

Štai tiek aš norėjau papasakoti apie savo patirtį dirbant su modulių bei integracijų testais. Kai pradedi rimtai rašyti testus, paaiškėja, kad tai yra kitoks programavimas, reikalaujantis naujų žinių ir kiek kitokio mąstymo.

Rodyk draugams

Modulių testų rekomendacijos – modulių ar integracijų testas?

Parašė Sergejus | 2011-08-07 22:11

Per paskutines dvi savaites teko rašyti ypatingai daug modulių testų (angl. unit tests) ir net pavyko pasiekti „išsvajotą“ 100% kodo aprėptį (angl. code coverage). Iš to gimė mintis parašyti kelių straipsnių ciklą apie rekomendacijas rašant modulių testus.

Modulių testų rekomendacijos – įrankiai ir aplinkos paruošimas
Modulių testų rekomendacijos – 13 modulių testų rašymo taisyklių
Modulių testų rekomendacijos – modulių testas ar integracijų testas?

Praeitoje dalyje aš pateikiau 13 modulių testų rašymo taisyklių. Straipsnis sukėlė labai gerą diskusiją (ypatingai Facebooke) ir vėliau buvo atnaujintas atsižvelgiant į jūsų pastabas. Šioje dalyje išnagrinėsime skirtumus tarp modulių testų ir integracijų testų bei patarimus rašant pastaruosius.

Modulių ar integracijų testas?

Labai dažnai modulių testų naujokai painioja modulių testus su integracijų testais ir vadina juos vienu vardu – modulių testais. Tai paaiškinama tuo, kad integracijų testai rašomi naudojant modulių testų karkasus bei tas pačias pagalbines bibliotekas. Nepaisant šio fakto, labai svarbu suprasti ideologinį skirtumą tarp jų, nes integracijų testų rašymas bei vykdymas turi savų niuansų.

Tai kas gi yra integracijų testas? Tam, kad atskirti modulių testą nuo integracijų testo, sau aš sugalvojau tokį apibrėžimą: jeigu testas naudoja bet kokius išorinius resursus, pavyzdžiui, failų sistemą, duomenų bazę, Web servisą ir panašiai ar testuoja iš karto kelis sistemos lygius, pavyzdžiui, prieigą prie duomenų kartu su verslo logika – jis yra integracijų testas.

Integracijų testams taikytina dauguma modulių testų rašymo taisyklių apart kelių, žemiau išvardinti skirtumai tarp jų:

  1. Integracijų testų vykdymas gali būti ilgesnis nei modulių, todėl visada patartina juos atskirti į atskirą rinkinį (angl. assembly) arba kategoriją (kai kurie modulių testų karkasai turi galimybę operuoti testų kategorijomis)
  2. Integracijų testai gali testuoti tiek viena sistemos lygį (pavyzdžiui, verslo logiką), tiek iš karto kelis (pavyzdžiui, prieiga prie duomenų kartu su verslo logika)
  3. Integracijų testai turi mokėti paruošti visus testo vykdymui reikalingus duomenis bei grąžinti sistemą į pradinę būseną testui pasibaigus, tam geriausiai tinka try-finally struktūra

Aukščiau išvardintus skirtumus paaiškinsiu tokiu pavyzdžių (tikrinamas vienas sistemos lygis – prieiga prie duomenų):

[Test]
[Category("Integration")]
public void Given_Not_Existing_Directory_Path_New_Directory_Is_Successfully_Created_After_Calling_CreateDirectoryIfNotExists()
{
  // Arrange
  var exists = true;
  var dirPath = Path.GetTempPath();
  var fs = new FileSystem();

  try
  {
    // Act
    fs.CreateDirectoryIfNotExists(dirPath);

    // Assert
    var doesDirExist = Directory.Exists(dirPath);
    Assert.AreEqual(exists, doesDirExist);
  }
  finally
  {
    if(Directory.Exists(dirPath)
    {
      Directory.Delete(dirPath)
    }
  }
}

Atkreipkite dėmesį, kad testas po savęs grąžina sistemą į pradinę būseną, t.y. ištrina ką tik sukurtą katalogą ir tai daroma finally bloke.

Kitoje dalyje mes aptarsime kas yra kodo aprėptis ir koks aprėpties procentas yra rekomenduojamas.

Rodyk draugams

Modulių testų rekomendacijos – 13 modulių testų rašymo taisyklių

Parašė Sergejus | 2011-07-31 18:50

Per paskutines dvi savaites teko rašyti ypatingai daug modulių testų (angl. unit tests) ir net pavyko pasiekti „išsvajotą“ 100% kodo aprėptį (angl. code coverage). Iš to gimė mintis parašyti kelių straipsnių ciklą apie rekomendacijas rašant modulių testus.

Modulių testų rekomendacijos – įrankiai ir aplinkos paruošimas
Modulių testų rekomendacijos – 13 modulių testų rašymo taisyklių

Praeitoje dalyje mes išnagrinėjome pagrindinius įrankius, skirtus modulių testavimui bei aptarėme aplinkos paruošimą. Šiame straipsnyje pateiksiu 13 modulių testų rašymo taisyklių, kurių pats prisilaikau jau keletą metų.

ATNAUJINTA

Atsižvelgiant į skaitytojų siūlymus (tiek tinklaraščio komentaruose, tiek Facebook), atnaujinau modulių testų rašymo taisykles.

13 modulių testų rašymo taisyklių

  1. Modulių testų rašymas tai irgi yra programavimas, todėl taikytinos visos standartinės kodo rašymo rekomendacijos
  2. Svarbiausias dalykas kuris turi būti ištestuotas – verslo logika
  3. Reikia turėti pakankamai modulių testų: nei per mažai, nei per daug
  4. Kiekvienas modulių testas turi tikrinti vieną ir tik vieną funkcionalumą
  5. Pagal galimybes reikia izoliuoti testuojamą logiką nuo išorinių resursų (naudojant netikrus objektus ir elgsenos aprašymą (angl. mocking), jeigu nepavyksta to padaryti, dažniausiai tai reiškia dizaino problemą kode ar statinių klasių panaudojimą
  6. Modulių testų vykdymas turi būti greitas
  7. Testų rezultatai tūri būti stabilūs ir determinuoti, tai yra visą laiką grąžinti tą patį rezultatą
  8. Modulių testų rašymo metu naudokite bazinę klasę bei pagalbinius metodus, bet jokių būdų nekvieskite tiesiogiai kitų testų
  9. Jeigu vienos klasės testai turi tą pačią sukūrimo / išvalymo logiką, naudokite specialiai tam skirtas konstrukcijas (Init, SetUp ir panašiai, priklausomai nuo pasirinkto modulių testų karkaso)
  10. Testuokite tik viešas (angl. public) ir vidinio naudojimo (angl. internal) klases, jeigu atsiranda poreikis testuoti privačius metodus (angl. private), dažniausiai tai reiškia šį funkcionalumą reikia iškelti į atskirą klasę
  11. Testo pradžioje apibrėžkite visus parametrus kaip kintamuosius, suteikiant jiems prasmingus pavadinimus
  12. Geriausias būdas dirbti su resursais – naudoti struktūrą try-finaly Kaip teisingai buvo pastebėta kelių skaitytojų – tai labiau liečia integracijų testus, todėl nusprendžiau išimti iš sąrašo
  13. Visi testai turi tenkinti [Arrange]-Act-Assert stilių, kur Arrange dalis nėra privaloma
  14. Testų pavadinimai turėtų pilnai nusakyti sąlygas, testuojamą objektą bei laukiamą rezultatą, pavyzdžiui galima naudoti tokį modulių testų vardų sudarymo šabloną: [Condition][Object][ExpectedResult][Action/Method]

Tikriausiai sutiksite, didžioji dalis aukščiau išvardintų taisyklių yra tiek logiškos, tiek savaime suprantamos. Paskutines 3 taisykles paaiškinsiu dviem pavyzdžiais:

public void Given_Not_Empty_String_It_Is_Correctly_Reversed()
{
  // Arrange
  var text = "Test text";
  var reversedText = "txet tseT";

  // Act  
  var result = StringHelper.Reverse(text);

  // Assert
  Assert.AreEqual(reversedText, result);
}
public void Given_Not_Existing_Directory_Path_Writer_Sucessfully_Creates_Directory_On_Write()
{
  // Arrange
  var exists = true;
  var doesNotExist = false;
  var dirPath = Path.GetTempPath();
  var text = "Test text";
  var fileSystem = new Mock<IFileSystem>();
  var writer = new FileWriter(fileSystem.Object);

  fileSystem.Setup(fs => fs.CreateDirectoryIfNotExists(It.IsAny<string>())

  // Act
  writer.Write(dirPath, text);

  // Assert
  fileSystem.Verify(fs => fs.CreateDirectoryIfNotExists(dirPath), Times.Once());
}

Arrange dalyje apibrėžiame visus reikalingus objektus ir parametrus bei esant poreikiui aprašome netikrus objektus ir reikalingą jų elgseną. Act dalyje iškviečiame testuojamą metodą su norimais parametrais. Assert dalyje patikriname vieną iš dviejų: arba paprastą Assert sąlygą, arba netikro objekto elgseną. Galiausiai, atkreipkite dėmesį į metodo pavadinimus – vien juos perskaičius galima tiksliai pasakyti kas daroma, esant kokioms pradinėms sąlygoms ir kokio rezultato tikimės.

Iš patirties galiu pasakyti, kad šios taisyklės ženkliai padeda programuotojams be didelės modulių testavimo patirties žengti pirmus žingsnius. Kitoje dalyje aptarsime ideologinius skirtumus tarp modulių testų ir integracijų testų.

Rodyk draugams

Modulių testų rekomendacijos – įrankiai ir aplinkos paruošimas

Parašė Sergejus | 2011-07-27 21:58

Per paskutines dvi savaites teko rašyti ypatingai daug modulių testų (angl. unit tests) ir net pavyko pasiekti „išsvajotą“ 100% kodo aprėptį (angl. code coverage). Iš to gimė mintis parašyti kelių straipsnių ciklą apie rekomendacijas rašant modulių testus.

Įrankiai

Pradėsime nuo pagrindinių bibliotekų (labai rekomenduoju šiems tikslams naudotis NuGet):

  • NUnit – paskutiniu metu naudoju kaip pagrindinį modulių testų karkasą, iš alternatyvų galite pasirinkti: MSTest, xUnit, MbUnit, MSpec.
  • Moq – populiariausia netikrų objektų kūrimo bei elgsenos aprašymo (angl. mocking) biblioteka, iš alternatyvų galite pasirinkti: Rhino Mocks, Typemock, JustMock.
  • AutoFixture – greitai populiarėjanti testinių duomenų generavimo biblioteka, iš alternatyvų galite pasirinkti: NBuilder.
  • ReSharper Unit Test Runner – viena patogiausių modulių testų vykdymo aplinkų (eina kartu su ReSharper), iš alternatyvų galite pasirinkti: MS Test Runner, TestDriven.NET, Gallio.
  • dotCover – naujas kodo aprėpties analizės įrankis (integruojamas su ReSharper, bet perkamas atskirai), iš alternatyvų galite pasirinkti: NCover, PartCover.

Aplinkos paruošimas

Turint po ranka visas reikalingas bibliotekas, pats laikas pereiti prie aplinkos paruošimo. Man pasiteisino tokia langų schema:

screen

Kairėje pusėje yra testuojama klasė (angl. System Under Test, SUT), o dešinėje – testai. Priklausomai nuo to, kiek monitorių turite, modulių testų vykdymo langas gali dinamiškai atsirasti lango apačioje arba gali būti užfiksuotas per pusę antro monitoriaus.

Kitas svarbus aspektas – greiti klavišai, skirti paleisti visus / vieną testą, praleisti aprėpties analizę ir pan. Kadangi dažnas testų paleidimas yra neatsiejama testų rašymo dalis, žymiai efektyviau išmokti greituosius klavišus ir naudotis jais, o ne spaudinėti grafinius mygtukus (panašiai, kaip yra su Ctrl + Shift + B :) ).

 

Štai mes ir esame pasiruošę rašyti modulių testus. Kitoje dalyje aš aptarsiu modulių testų vardų sudarymo rekomendacijas, kurias surinkau ir apibendrinau per keletą paskutinių metų.

Rodyk draugams