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.
În contextul testării unității, o unitate are mai multe caracteristici.
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:
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.
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:
SqlClient
furnizor de fabrică.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.
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.
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:
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ă.
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ă.
Î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".
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.
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.
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:
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ă.
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.
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 testareMai 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.
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.
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.
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.
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:
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.