Unitatea de testare succintă Ce este unitatea de testare?

Acesta este un extras din Cartea electronică de testare a unității, de Marc Clifton, oferită cu amabilitate de Syncfusion.

Testarea unităților se referă la dovedirea corectitudinii. Pentru a dovedi că ceva funcționează corect, trebuie mai întâi să înțelegeți ce a unitate și a Test de fapt sunt înainte de a putea explora ceea ce este demonstrat în cadrul capabilităților de testare unitate.

Ce este o unitate?

În contextul testării unității, o unitate are mai multe caracteristici.

Unități pure

O unitate pură este metoda cea mai simplă și cea mai ideală pentru scrierea unui test de unitate. O unitate pură are mai multe caracteristici care facilitează testarea ușoară.

O unitate ar trebui (în mod ideal) să nu apeleze alte metode

În ceea ce privește testarea unitară, o unitate ar trebui să fie în primul rând o metodă care face ceva fără a apela alte metode. Exemple de aceste unități pure pot fi găsite în Şir și Math clase - majoritatea operațiunilor efectuate nu se bazează pe nici o altă metodă. De exemplu, următorul cod (luat din ceva scris de autor)

public void SelectedMasters () șir curentEntity = dgvModel.DataMember; șir navToEntity = cbMasterTables.SelectedItem.ToString (); DataGridViewSelectedRowCollection selectedRows = dgvModel.SelectedRows; StringBuilder qualifier = BuildQualificator (selectatRede); UpdateGrid (navToEntity); SetRowFilter (navToEntity, qualifier.ToString ()); ShowNavigateToMaster (navToEntity, qualifier.ToString ()); 

nu ar trebui considerată o unitate din trei motive:

  • Mai degrabă decât să ia parametrii, acesta obține valorile implicate în calculul de la obiectele interfeței utilizator, în special un DataGridView și un ComboBox.
  • Face câteva apeluri la alte metode potențial sunteți Unități.
  • Una dintre metode pare să actualizeze afișarea, încurcând un calcul cu o vizualizare.

Primul motiv evidențiază o problemă subtilă - proprietățile ar trebui să fie considerate apeluri metodice. De fapt, ele se află în implementarea de bază. Dacă metoda dvs. utilizează proprietăți ale altor clase, acesta este un fel de apel de metodă și ar trebui să fie luat în considerare cu atenție la scrierea unei unități adecvate.

Realist, acest lucru nu este întotdeauna posibil. Adesea, este necesar un apel la cadrul sau la un alt API pentru ca unitatea să-și îndeplinească cu succes activitatea. Cu toate acestea, aceste apeluri ar trebui inspectate pentru a determina dacă metoda ar putea fi îmbunătățită pentru a face o unitate mai pură, de exemplu, prin extragerea apelurilor într-o metodă mai ridicată și prin transmiterea rezultatelor apelurilor ca parametru la unitate.

O unitate ar trebui să facă doar un singur lucru

Un corolar al "unei unități nu trebuie să apeleze la alte metode" este că o unitate este o metodă care are un singur lucru și numai un singur lucru. Adesea, alte metode sunt numite pentru a face mai mult decât un singur lucru-o abilitate valoroasă de a ști când ceva constă de fapt din mai multe sub-sarcini - chiar dacă poate fi descris ca o sarcină la nivel înalt, ceea ce o face să pară ca o singură sarcină!

Codul următor ar putea arăta ca o unitate rezonabilă care face un singur lucru: introduce un nume în baza de date.

public int Insert (Persoană persoană) DbProviderFactory factory = SqlClientFactory.Instance; utilizând (conexiunea DbConnection = factory.CreateConnection ()) connection.ConnectionString = "Server = localhost; Database = myDataBase; Trusted_Connection = True;"; connection.Open (); folosind (DbCommand command = connection.CreateCommand ()) command.CommandText = "introduceți în valorile PERSON (ID, NAME) (@Id, @Name)"; command.CommandType = CommandType.Text; DbParametrul id = command.CreateParameter (); id.ParameterName = "@Id"; id.DbType = DbType.Int32; id.Value = person.Id; DbParametrul nume = command.CreateParameter (); nume.ParameterName = "@Name"; name.DbType = DbType.String; name.Size = 50; name.Value = person.Name; command.Parameters.AddRange (nou DbParameter [] id, nume); int rowsAffected = comanda.ExecuteNonQuery (); rândurile returnateAlfectat; 

Cu toate acestea, acest cod face de fapt mai multe lucruri:

  • Obținerea unui SqlClient furnizor de fabrică.
  • Instalați o conexiune și deschideți-o.
  • Instanțializarea unei comenzi și inițializarea comenzii.
  • Crearea și adăugarea la comandă a doi parametri.
  • Executarea comenzii și returnarea numărului de rânduri afectate.

Există o varietate de probleme cu acest cod care îl descalifică de la a fi o unitate și fac dificilă reducerea în unități de bază. O modalitate mai bună de a scrie acest cod ar putea să arate astfel:

public int RefactoredInsert (Persoană persoană) DbProviderFactory factory = SqlClientFactory.Instance; folosind (DbConmand cmd = CreateTextCommand (conn, "introduceți în valorile PERSON (ID, NAME) (@ Id, @ Nume) ")) AddParameter (cmd," @Id ", person.Id); AddParameter (cmd, "@ Nume", 50, persoană.Număr); int rowsAffected = cmd.ExecuteNonQuery (); rândurile returnateAlfectat;  protejată DbConnection OpenConnection (fabrică DbProviderFactory, string connectString) DbConnection conn = factory.CreateConnection (); conn.ConnectionString = connectString; conn.Open (); retur conn;  protejat DbCommand CreateTextCommand (DbConnection conn, șir cmdText) DbCommand cmd = conn.CreateCommand (); cmd.CommandText = cmdText; cmd.CommandType = CommandType.Text; retur cmd;  protejat void AddParameter (DbCommand cmd, șir paramName, int paramValue) DbParameter param = cmd.CreateParameter (); param.ParameterName = paramName; param.DbType = DbType.Int32; param.Value = paramValue; cmd.Parameters.Add (param);  protejat void AddParameter (DbCommand cmd, șir paramName, dimensiune int, șir paramValue) DbParameter param = cmd.CreateParameter (); param.ParameterName = paramName; param.DbType = DbType.String; param.Size = dimensiune; param.Value = paramValue; cmd.Parameters.Add (param); 

Observați cum, pe lângă căutarea mai curată, metodele OpenConnection, CreateTextCommand, și AddParameter sunt mai potrivite pentru testarea unitară (ignorând faptul că acestea sunt metode protejate). Aceste metode fac doar un singur lucru și, ca unități, pot fi testate pentru a se asigura că fac acest lucru corect. Din acest motiv, nu este indicat să se testeze RefactoredInsert deoarece se bazează în întregime pe alte funcții care au teste unitare. În cel mai bun caz, s-ar putea dori să scrieți câteva cazuri de testare a excepțiilor și, eventual, o validare pe câmpurile din Persoană masa.

Cod corect dovedit

Ce se întâmplă dacă metoda de nivel superior are ceva mai mult decât să apeleze alte metode pentru care există teste unitare, cum ar fi un fel de calcul suplimentar? În acest caz, codul care efectuează calculul trebuie mutat pe propria sa metodă, trebuie să fie scrise testele pentru acesta și din nou metoda de nivel superior se poate baza pe corectitudinea codului pe care îl numește. Acesta este procesul de construire a codului probabil corect. Corectitudinea metodelor de nivel superior se îmbunătățește atunci când tot ce fac este să numească metode de nivel inferior care au dovezi de corectitudine.

O unitate nu ar trebui (în mod ideal) să aibă mai multe căi de cod

Complexitatea ciclomatică reprezintă un proces de testare unitară și de testare a aplicațiilor în general, deoarece crește dificultatea de a testa toate căile de cod. În mod ideal, o unitate nu va avea niciunul dacă sau intrerupator declarații. Corpul acestor afirmații ar trebui considerat ca unități (presupunând că îndeplinesc celelalte criterii ale unei unități) și că trebuie să fie testabile, ar trebui extrase în propriile lor metode.

Iată un alt exemplu preluat din proiectul autorului MyXaml (parte a parserului):

dacă (nodul este XmlComment)) objectNode = node; (nodul XmlNode în topElement.ChildNodes) if (! pauză;  foreach (XmlAttribute attr în objectNode.Attributes) dacă (attr.LocalName == "Nume") nameAttr = attr; pauză;  altceva ... etc ...

Aici avem mai multe căi de cod care implică dacă, altfel, și pentru fiecare declarații, care:

  • Creați complexitatea setărilor, deoarece trebuie îndeplinite multe condiții pentru a executa codul interior.
  • Creați complexitatea de testare, deoarece căile de cod necesită setări diferite pentru a vă asigura că fiecare cale de cod este testată.

Evident, ramificația condiționată, buclele, declarațiile de caz etc. nu pot fi evitate, dar ar putea fi util să se ia în considerare refactorizarea codului astfel încât interioarele condițiilor și buclelor să fie metode separate care pot fi testate independent. Apoi, testele pentru metoda de nivel superior pot pur și simplu să asigure că stările (reprezentate de condiții, bucle, comutatoare etc.) sunt gestionate corect, independent de calculele pe care le efectuează.

Unități dependente

Metodele care au dependențe de alte clase, date și informații de stare sunt mai complexe pentru a fi testate, deoarece aceste dependențe se traduc în cerințe pentru obiectele instanțiate, existența datelor și starea predeterminată.

precondiţii

În cea mai simplă formă, unitățile dependente au precondiții care trebuie îndeplinite. Motoarele pentru testul unității oferă mecanisme pentru a concretiza dependențele de testare, atât pentru teste individuale, cât și pentru toate testele din cadrul unui grup de testare sau "dispozitiv de fixare".

Servicii actuale sau simulate

Unitățile dependente complicate au nevoie de servicii cum ar fi conexiunile bazei de date pentru a fi instanțiate sau simulate. În exemplul codului anterior, Introduce metoda nu poate fi testată pe unitate fără capacitatea de a se conecta la o bază de date actuală. Acest cod devine mai testabil dacă interacțiunea bazei de date poate fi simulată, de obicei prin utilizarea unor interfețe sau clase de bază (abstracte sau nu).

Metodele refactate în Introduce codul descris mai devreme este un bun exemplu pentru că DbProviderFactory este o clasă de bază abstractă, astfel încât se poate crea cu ușurință o clasă care derivă din DbProviderFactory pentru a simula conexiunea bazei de date.

Manipularea excepțiilor externe

Unitățile dependente, deoarece fac apeluri către alte API-uri sau metode, sunt, de asemenea, mai fragile - ar putea fi nevoite să se ocupe în mod explicit de erorile generate de metodele pe care le apelează. În proba de cod anterioară, Introduce codul metodei poate fi înfășurat într-un bloc try-catch, deoarece este cu siguranță posibil ca conexiunea bazei de date să nu existe. Operatorul de excepții s-ar putea întoarce 0 pentru numărul de rânduri afectate, raportând eroarea printr-un alt mecanism. Într-un astfel de scenariu, testele unității trebuie să fie capabile să simuleze această excepție pentru a se asigura că toate căile de cod sunt executate corect, inclusiv captură și in cele din urma blocuri.


Ce este un test?

Un test oferă o afirmație utilă despre corectitudinea unității. Testele care afirmă corectitudinea unei unități folosesc de obicei unitatea în două moduri:

  • Testați modul în care unitatea se comportă în condiții normale.
  • Testați modul în care unitatea se comportă în condiții anormale.

Testarea condițiilor normale

Testarea modului în care unitatea se comportă în condiții normale este de departe cel mai ușor test de scris. La urma urmei, atunci când scriem o funcție, fie o scriem pentru a satisface o cerință explicită sau implicită. Implementarea reflectă o înțelegere a acelei cerințe, care în parte cuprinde ceea ce ne așteptăm ca intrări în funcție și cum ne așteptăm ca funcția să se comporte cu acele inputuri. Prin urmare, testează rezultatul funcției date date așteptate, indiferent dacă rezultatul funcției este o valoare de returnare sau o schimbare de stare. În plus, dacă unitatea este dependentă de alte funcții sau servicii, așteptăm de asemenea să se comporte corect și scriu un test cu presupunerea presupusă.

Analiza condițiilor anormale

Testarea modului în care unitatea se comportă în condiții anormale este mult mai dificilă. Este nevoie de a determina ce este o afecțiune anormală, care de obicei nu este evidentă prin inspectarea codului. Acest lucru este complicat atunci când se testează o unitate dependentă - o unitate care se așteaptă ca o altă funcție sau serviciu să se comporte corect. În plus, nu știm cum ar putea exercita unitatea un alt programator sau utilizator.


Teste de unitate și alte practici de testare

Testarea unităților ca parte a unei abordări cuprinzătoare de testare

Testarea unităților nu înlocuiește alte practici de testare; ar trebui să completeze alte practici de testare, oferind suport suplimentar în materie de documente și încredere. Figura 1 ilustrează un concept al "fluxului de dezvoltare a aplicațiilor" - modul în care alte teste se integrează cu testarea unității. Rețineți că clientul poate fi implicat în orice etapă, deși, de regulă, la procedura de test de acceptare (ATP), integrarea sistemului și etapele de utilizare.

Comparați acest lucru cu modelul V al procesului de dezvoltare și testare a software-ului. Deși este legat de modelul de dezvoltare a software-ului (care, în cele din urmă, toate celelalte modele de dezvoltare software sunt fie un subset, fie o extindere), modelul V oferă o imagine bună a tipului de testare necesar pentru fiecare strat de procesul de dezvoltare software:

Modelul V de testare

Mai mult, atunci când un punct de test nu reușește în alte practici de testare, o anumită bucată de cod poate fi identificată de obicei ca fiind responsabilă pentru eșec. În acest caz, devine posibil să se trateze acea bucată de cod ca o unitate și să se scrie un test de unitate pentru a crea mai întâi eșecul și, atunci când codul a fost schimbat, pentru a verifica fixul.

Proceduri de testare de acceptare

Procedura de testare de acceptare (ATP) este deseori utilizată ca cerință contractuală pentru a dovedi că anumite funcționalități au fost implementate. ATP-urile sunt deseori asociate cu repere, iar reperele sunt deseori asociate cu plățile sau cu alte finanțări pentru proiecte. Un ATP diferă de un test de unitate deoarece ATP demonstrează că a fost implementată funcționalitatea cu privire la întreaga cerință a elementului rând. De exemplu, un test de unitate poate determina dacă calculul este corect. Cu toate acestea, ATP ar putea valida faptul că elementele de utilizator sunt furnizate în interfața cu utilizatorul și că interfața de utilizator afișează rezultatul calculului specificat de cerință. Aceste cerințe nu sunt acoperite de testul unității.

Testarea interfeței cu utilizatorul automată

Un ATP ar putea fi inițial scris ca o serie de interacțiuni cu interfața cu utilizatorul (UI) pentru a verifica dacă cerințele au fost îndeplinite. Testarea prin regresie a aplicației pe măsură ce aceasta continuă să se dezvolte este aplicabilă testării unităților, precum și testelor de acceptare. Testarea automată a interfeței cu utilizatorul este un alt instrument complet separat de unitatea de testare care economisește timp și forță de muncă, reducând în același timp erorile de testare. Ca și în cazul ATP-urilor, testele unității nu înlocuiesc în niciun caz valoarea testelor interfeței automate cu utilizatorul.

Testare utilizabilitate și experiență utilizator

Testele de testare, ATP-urile și testele UI automate nu înlocuiesc în nici un fel testarea utilizabilității - punerea aplicației în fața utilizatorilor și obținerea feedback-ului privind "experiența utilizatorului". Testarea utilizabilității nu trebuie să vizeze găsirea defectelor de calcul (bug-uri) și, prin urmare, este complet în afara domeniului de aplicare a testelor de unitate.

Performanță și testarea încărcării

Unele unelte de testare unitate oferă un mijloc de măsurare a performanței unei metode. De exemplu, motorul de testare al Visual Studio raportează timpul de execuție, iar NUnit are atribute care pot fi folosite pentru a verifica dacă o metodă se execută într-un interval alocat.

În mod ideal, un instrument de testare a unităților pentru limbile .NET ar trebui să pună în aplicare în mod explicit testarea performanței pentru a compensa compilarea codului just-in-time (JIT) prima dată când codul este executat.

Cele mai multe teste de sarcină (și testele de performanță aferente) nu sunt potrivite pentru testele unitare. Anumite forme de teste de sarcină se pot face și cu testarea unității, cel puțin cu limitarea hardware-ului și a sistemului de operare, cum ar fi:

  • Simularea constrângerilor de memorie.
  • Simularea constrângerilor legate de resurse.

Totuși, aceste tipuri de teste necesită, în mod ideal, suportul API-ului framework sau OS pentru a simula aceste tipuri de sarcini pentru aplicația testată. Forțând întregului sistem de operare să consume o cantitate mare de memorie, resurse sau ambele, afectează toate aplicațiile, inclusiv aplicația de testare a unității. Aceasta nu este o abordare dorită.

Alte tipuri de testare a sarcinii, cum ar fi simularea mai multor cazuri de funcționare simultană a unei operații, nu sunt candidați pentru testarea unităților. De exemplu, testarea performanței unui serviciu web cu o încărcătură de un milion de tranzacții pe minut nu este probabil posibilă utilizând o singură mașină. Deși acest tip de test poate fi scris cu ușurință ca unitate, testul propriu-zis ar implica o suită de mașini de testare. Și în cele din urmă, ați testat doar un comportament foarte restrâns al serviciului web în condiții de rețea specifice, care în nici un caz nu reprezintă realitatea.

Din acest motiv, performanța și testarea încărcării au o aplicație limitată cu testarea unităților.

Cod