Exceptii
Aplicatiile intalnesc erori la executie. Aplicatiile robuste trebuie sa manipuleze erorile cat mai elegant cu putinta. Cateva motive de aparitie a erorilor:
-
Aplicatia nu are comportamentul asteptat
-
rezultat al unui bug
-
factori din spatele aplicatiei: conexiune la baza de date intrerupta, functionarea driver-elor, etc
In Java, atunci cand folosim resurse externe, compilatorul cere sa manipulam exceptiile ce pot aparea (spre ex: java.nio, java.sql). A manipula o exceptie inseamna sa adaugam in cod un bloc de manipulare a erorii (try-catch). A declara o exceptie inseamna ca declaram ca o metoda poate esua in executie sa (clauza throws).
O exceptie reprezinta, asadar, un eveniment ce intrerupe executia normala a unui program.
Cand se intalneste o exceptie la rulare, metoda in care a aparut exceptia creaza un obiect exceptie. Metoda poate arunca exceptia inapoi catre metoda apelanta, care probabil o va trata. Obiectul exceptie include informatii despre exceptie, cum ar fi metoda in care exceptia a aparut si cauza exceptiei. Metoda apelanta poate trimite exceptia, mai departe, unei alte metode, care la randul sau a apelat-o. Daca nicio metoda nu manipuleaza exceptia, programul se termina cand obiectul exceptie atinge metoda main().
Cand apare o exceptie o putem preveni trimitand-o in stiva de apel sau tratand-o, astfel aplicatia continundu-si executia pe o alta cale.
Blocul try cuprinde codul ce poate cauza o exceptie. Acest bloc este numit si cod protejat (protected code). Un bloc try este urmat de zero sau mai multe blocuri catch ce specifica tipurile de exceptie ce pot fi prinse.
catch ar trebui sa fie folosit pentru:
-
a reincerca executia unei operatii
-
incercarea de a executa o alta operatie
-
o iesire eleganta sau apelul lui return
Scrierea unui bloc catch vid este o bad practice.
Blocurile catch se executa in ordinea in care au fost scrise, de sus in jos, pina la intilnirea primei exceptii ce se potriveste ca tip cu cel al tipului aruncat. Cel mult un bloc catch se executa. De aceea, vom plasa blocul catch, cel mai general, pe ultima pozitie in blocurile din sirul blocurilor catch. Daca nu se arunca nicio exceptie atunci nici un bloc catch se executa. Blocul finally este optional, daca try este urmat de macar un catch. Blocul finally se executa indiferent daca avem sau nu aruncata exceptie, cu o singura exceptie, atunci cand executam System.exit.
// sintaxa de baza a lui try
try {
// cod ce poate arunca o exceptie
}
catch (TipExceptie1 Identificator1) {
// cod de manipulare a exceptiei
}
catch (TipExceptie2 Identificator2) {
// cod de manipulare a exceptiei
}
finally {
// 0 sau 1 clauze finally
}
Cand deschidem resurse precum fisiere sau conexiuni la baza de date, trebuie intotdeauna sa le inchidem. Incercarea de a le inchide in try poate fi problematica deoarece putem incheia blocul sarind operatia. De aceea este recomandata inchiderea acestora in blocul finally. Uneori operatia pe care o executam in blocul finally poate genera o exceptie. In acest caz va trebui sa scrie un bloc try-catch in interiorul lui finally. Putem avea try-catch si in interiorul lui try sau al lui catch.
public class FinallyExampleMain {
public static void main(String[] args) {
InputStream in = null;
try {
System.out.println("About to open a file");
in = new FileInputStream("missingfile.txt");
System.out.println("File open");
int data = in.read();
} catch (FileNotFoundException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
System.out.println("Failed to close file");
}
}
}
}
Toate exceptiile si erorile Java sunt subclase ale clasei Throwable din pachetul java.lang.
Putem loga exceptiile ce apar pe parcursul aplicatiei folosind Apache Log4j si framework-uri predefinite din java.util.
In Java SE7 avem la dispozitie un try cu resurse, ce va inchide automat resursele. Astfel eliminam folosirea lui finally. Orice clasa ce implementeaza java.lang.AutoCloseable poate fi utilizata ca resursa. Pentru ca o resursa sa fie automat inchisa, referinta sa trebuie declarata in interiorul parantezelor rotunde ale lui try.
public class FinallyExampleMain {
public static void main(String[] args) {
System.out.println("About to open a file");
try(InputStream in = new FileInputStream("missingfile.txt")) {
System.out.println("File open");
int data = in.read();
} catch (FileNotFoundException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
Putem deschide mai multe resurse daca ele sunt separate prin punct si virgula. Inchiderea lor se va face in ordine inversa deschiderii.
Daca o exceptie apare in momentul crearii unei resurse AutoCloseable, controlul va fi preluat imediat de blocul catch.
Daca o exceptie apare in blocul try, toate resursele vor fi inchise inainte de a rula catch. Daca o exceptie va fi generata in timpul inchiderii resursei, ea va fi suprimata. Controlul va fi preluat de un bloc catch. Exceptiile ar putea fi afisate astfel:
public class FinallyExampleMain {
public static void main(String[] args) {
System.out.println("About to open a file");
try (InputStream in = new FileInputStream("missingfile.txt")) {
System.out.println("File open");
int data = in.read();
} catch (Exception e) {
System.out.println(e.getMessage());
for (Throwable t : e.getSuppressed())
System.out.println(t.getMessage());
}
}
}
Resursele din try cu resurse trebuie sa implementeze unul dintre:
-
java.lang.AutoCloseable, care poate arunca un Exception
-
java.io.Closeable, care extinde AutoCloseable si care poate arunca o IOException
Interfata AutoCloseable are o singura metoda void close() throws Exception;
Metoda close() din AutoCloseable, spre deosebire de cea din Closeable, nu este idempotenta. Aceasta inseamna ca apelul sau mai mult decat o data poate avea efecte secundare. Cei care implementeaza metoda din AutoCloseable vor trebui sa o faca idempotenta
Java SE7 furnizeaza o clauza multi-catch. Avantajele deriva din faptul ca uneori dorim sa facem anumite actiuni independent de exceptia ce este generata. Multi-catch reduce cantitatea de cod scris, prin eliminarea necesitatii scrieri mai multor clauze catch cu acelasi comportament. Un alt avantaj este acela ca face mai putin probabila incercarea de a prinde o exceptie generica. Alternativele de tip, separate prin bara verticala, nu trebuie sa aiba o relatie de mostenire.
public class FinallyExampleMain {
public static void main(String[] args) {
System.out.println("About to open a file");
try (InputStream in = new FileInputStream("missingfile.tx");
Scanner s = new Scanner(in);) {
System.out.println("File open");
int data = s.nextInt();
} catch (NoSuchElementException | IOException e) {
System.out.println(e.getClass().getName());
System.out.println("Quiting");
}
}
}
Numai un obiect Throwable poate fi aruncat, si acesta include exceptii sau erori de sistem. Metodele din Throwable sunt:
-
getMessage(), ce returneaza un string cuprinzand mesajul de eroare al obiectului Throwable
-
toString(), returneaza o descriere a obiectului exceptie incluzand si tipul exceptiei
-
initCause(), seteaza cauza exceptiei care este intotdeauna un alt obiect Throwable, ceea ce permite inlantuirea exceptiilor
-
printStackTrace(), permite identificarea metodei ce a aruncat o anumita exceptie
Ierarhia Throwable este urmatoarea:
Clasa Throwable are doua subclase directe:
-
Error, si clasele derivate din ea sunt folosite pentru erori de sistem sau erori la compilare ce nu pot fi tratate de aplicatie, ca spre ex. ExceptionInInitializerError, StackOverflowError si NoClassDefFoundError
-
Exception, si clasele derivate din ea sunt utilizate pentru implementarea exceptiilor pe care o aplicatie se asteapta sa le intilneasca. Fiecare subclasa a lui Exception reprezinta un tip particular de exceptie
Clasa RuntimeException si subclasele sale descriu obiectele exceptie ce sunt aruncate automat de JVM, la rulare. Exceptiile la rulare sunt generate, in general, de bug-uri in codul sursa. Printre subclasele acesteia amintim:
-
ArithmeticException, pentru exceptiile ce cuprind incalcarea regulilor matematice, cum ar fi impartirea la zero
-
IndexOutOfBoundsException, cand indexul unui string sau array nu este in domeniul acceptat
-
NegativeArraySizeException, cand incercam sa cream un sir cu numar negativ de elemente
Pentru a prinde exceptiile la rulare putem folosi blocurile try-catch, dar acest lucru nu este obligatoriu. Daca try-catch nu este prezent, toate exceptiile la rulare vor fi considerate exceptii neverificate (unchecked exceptions). Aceasta inseamna ca compilatorul nu verifica daca ele au fost declarate sau manipulate.
Toate exceptiile ce mostenesc Exception dar nu mostenesc RuntimeException sunt cunoscute ca clase exceptie verificate. Acestea trebuie declarate intr-o metoda folosind clauza throws sau tratate explicit in corpul metodei. Altfel se va genera eroare la compilare. Din categoria acestor exceptii amintim:
-
ClassNotFoundException, este aruncata atunci cand un program incearca sa incarce o clasa, folosind numele ei, dar definirea clasei cu numele specificat nu este gasita. Aceste exceptii apar atunci cand un program utilizeaza metoda forName() din Class sau findSystemClass() sau loadClass() din ClassLoader
-
InterruptedException, aruncata atunci cand un fir a fost inactiv pentru mult timp si un alt fir ce foloseste metoda interrupt() din Thread incearca sa-l intrerupa
-
IllegalAccessException, aruncata atunci cand executia metodei curente nu are acces la definitia unui camp pe care doreste sa-l acceseze sau modifice sau la definitia unei metode pe care doreste sa o invoce
-
FileNotFoundException, apartine pachetului java.io si este aruncata atunci cand aplicatia nu poate deschide un fisier specificat (fisier inaccesibil sau inexistent).
Exceptiile pot fi tratate in interiorul metodei sau in metoda sa apelanta. Cand o exceptie a fost aruncata trebuie sa implementam interfata ExceptionListener ce receptioneaza doar exceptii si apeleza metoda exceptionThrown().
Pentru a ne asigura ca o metoda poate arunca exceptii utilizam cuvantul rezervat throws cand declaram metoda. In clauza throws specificam tipurile de exceptii ce pot fi aruncate, tipuri separate prin virgula.
In metoda apelanta exceptia poate fi tratata sau trimisa metodei apelante a acesteia adaugand clauzei throws exceptiile netratate.
Daca o metoda suprascrie o metoda din clasa de baza ea poate arunca numai aceleasi exceptii ca si metoda din clasa de baza, o submultime a lor sau subclase ale exceptiilor. Metoda suprascrisa nu poate arunca exceptii ce sunt superclase ale claselor exceptie ale metodei din clasa de baza, sau exceptii ce apartin unei alte ierarhii. Aceste reguli se aplica numai exceptiilor verificate.
Fie urmatorul exemplu:
public class ThrowsMain {
public static void main(String[] args) {
try {
int data = readByteFromFile();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
public static int readByteFromFile() throws IOException {
try (InputStream in = new FileInputStream("a.txt")) {
System.out.println("File open");
return in.read();
}
}
}
Nu am utilizat catch in definitia metodei readByteFromFile(). Rolul lui try este doar acela al destionarii resurselor. Tot in semnatura metodei am declarat aruncarea lui IOException, fara a specifica explicit aruncarea lui FileNotFoundException. Aceasta datorita faptului ca cele doua clase sunt ierarhice. Totusi, este o buna practica sa le aruncam pe ambele.
O aplicatie Java SE trebuie sa-si manipuleze exceptile inainte de a fi aruncate de main(). main() este posibil sa arunce exceptii, dar acest lucru este recomandat a fi evitat.
Putem rearunca o exceptie ce deja a fost prinsa.
public static int readByteFromFile() throws IOException {
try (InputStream in = new FileInputStream("a.txt")) {
System.out.println("File open");
return in.read();
}
catch(IOException e){
System.out.println(e.getMessage());
throw e;
}
}
Java SE7 suporta rearuncarea precisa a tipului exceptie. Urmatorul exemplu nu s-ar compila in Java SE6 pentru ca catch primeste o Exception, dar arunca o IOException.
public static int readByteFromFile() throws IOException {
try (InputStream in = new FileInputStream("a.txt")) {
System.out.println("File open");
return in.read();
}
catch(Exception e){
System.out.println(e.getMessage());
throw e;
}
}
Putem crea clase exceptii custom prin extinderea clasei Exception sau a uneia dintre subclase ei.
package exceptions;
public class DAOException extends Exception {
private static final long serialVersionUID = 1L;
public DAOException() {
super();
}
public DAOException(String message) {
super(message);
}
}
Exceptiile custom nu sunt aruncate niciodata de clasele standard Java. Sta in priceperea dezvoltatorului aruncarea lor.
Clasa exceptie standard poate suprascrie metode sau adauga noi functionalitati. Deoarece exceptiile captureaza informatii despre o problema aparuta va trebui sa adaugam campuri si metode dependente de tipul de informatie pe care dorim sa-l capturam. Daca un string poate captura toate informatiile necesare putem utiliza metoda getMessage() pe care toate clase exceptie o mostenesc din Throwable. Orice constructor de exceptie ce primeste un string il va stoca, astfel incat sa fie returnat cu getMessage().
Java permite ca exceptiile sa fie create si aruncate:
-
implicit, exceptiile sunt create si aruncate automat de catre JVM, la rulare
-
explicit, exceptiile sunt create explicit prin specificarea in cod ca un anumit tip de obiect exceptie trebuie sa fie creat si aruncat atunci cand o anumita situatie este intalnita. Acest tip de tratare este folositor daca dorim ca metoda sa trimita exceptia metodei apelante, dar in anumite circumstante. Pentru a arunca o exceptie explicit se utilizeaza cuvantul rezervat throw, pentru orice obiect ce este o instanta sau o subclasa a lui Throwable. Noul obiect exceptie trebuie sa fie creat in aceeasi linie unde se afla throw. Aceasta deoarece urma stivei de apel arata unde obiectul exceptie a fost creat si nu unde a fost aruncat. Cand cream si aruncam o exceptie explicit putem adauga propriul mesaj de eroare. Aceasta este posibil deoarece clasa Throwable are un constructor cu argument string. Mesajul customizat va fi afisat cand aplicatia se termina.
In versiunile de dinainte de 1.4 era dificil de comunicat cauzele unui esec peste doua sau mai multe layere abstracte ale aplicatiei. O solutie era trimiterea exceptiei de la un layer la altul. Aceasta crea o dependenta intre layere care trebuiau sa fie distincte. Aceasta solutie este dezavantajoasa. Alta solutie ar fi extinderea clasei Exception pentru a asocia o cauza (cause) Throwable unei exceptii particulare, dar si aceasta solutie este inacceptabila atunci cand avem nevoie sa folosim altundeva API-ul.
Inlantuirea exceptiilor incepand cu versiunea 1.4, Java ne permite sa pastram urma exceptiei originale ce a fost aruncata. Aceasta se face pe baza proprietatii cause si a inca doi constructori ai clasei Throwable ce admit cause ca parametru. Setarea cauzei directe a fiecarei exceptii aruncate determina posibilitatea urmaririi complete a istoriei exceptiei. O singura exceptie contine lantul exceptiilor cu care se afla in legatura. Cauza unui obiect Throwable este o referinta la un alt obiect Throwable care a cauzat aruncarea primului. Proprietatea cause nu poate fi accesata direct, dar poate fi setata respectiv accesata prin:
-
initCause() sau printr-un constructor al clasei Throwable
-
getCause()
Exista patru constructori ai clasei Throwable care sunt implementati in clasele Exception, RuntimeException si Error. Dintre acestia doi initializeaza cauza.
public Throwable();
public Throwable(String message);
public Throwable(Throwable cause);
public Throwable(String message, Throwable cause);
In exemplul urmator putem observa modul in care a fost creata o cauza si apoi cum ea a fost urmarita:
public class Test{
static void metoda (int x, int y) throws Exception{
try {
for (int i = 1; i >= x ; i--) {
System.out.println (y /i) ;
}
}
catch (ArithmeticException e) {
Exception myExcept = new Exception("Eroare a unei operatii aritmetice") ;
myExcept.initCause (e) ;
throw myExcept ;
}
}
public static void main (String args[]) {
try {
metoda(-5,6);
} catch (Exception e) {
System.out.println (e.getMessage()) ;
System.out.println (e.getCause().getMessage()) ;
}
}
}
Uneori este necesar sa suprascriem metoda getCause(), spre exemplu atunci cand tratam exceptii in cod legacy, in versiuni ale Javei in care se folosea doar cod legacy si proprietatea cause nu exista.
Utilizatorii pot sa-si creeze propriile exceptii cu ajutorul carora pot oferi mai multe informatii despre cum exceptia a aparut sau sa manipuleze, dependent de aplicatie, exceptia. Pentru aceasta vom crea o noua clasa ce extinde Exception sau o clasa ce a extins Exception. Clasa exceptie utilizator nu poate extinde Throwable pentru ca aceasta este clasa de baza atat pentru exceptii cat si pentru erori. De asemenea, nu poate extinde clasa Error, aceasta fiind folosita pentru erori pe care aplicatia nu le poate recupera.
Nu este recomandat sa creem subclase utilizator ale clasei RuntimeException si aceasta deoarece obiectele acestea ar trebui sa fie generate si aruncate de JVM. Utilizatorul nu trebuie sa se ingrijeasca de aceste exceptii.
Pentru a ascunde tipul unei exceptii ce a fost generate utilizam o exceptie wrapper. Fie urmatorul exemplu:
import entities.Entity;
import exceptions.DAOException;
public class EntityDAO {
private Entity[] entityArray;
public EntityDAO(int val) {
entityArray = new Entity[val];
}
public Entity findById(int id) throws DAOException {
try {
return entityArray[id];
} catch (ArrayIndexOutOfBoundsException e) {
throw new DAOException("out of bounds", e);
}
}
}
package exceptions;
public class DAOException extends Exception {
private static final long serialVersionUID = 1L;
public DAOException(Throwable cause) {
super(cause);
}
public DAOException(String message, Throwable cause) {
super(message, cause);
}
}
import daos.EntityDAO;
import exceptions.DAOException;
public class TestEntityDao {
public static void main(String[] args) {
EntityDAO ent = new EntityDAO(3);
try {
ent.findById(5);
} catch (DAOException e) {
Throwable t = e.getCause();
System.out.println(e.getMessage() + t.getMessage());
}
}
}
O implementare DAO poate utiliza o exceptie wrapper pentru a pastra abstractia si pentru a evita netratarea exceptiilor. Iata explicatia: majoritatea metodelor din DAO trebuie sa trateze exceptii (IOExceptions sau SQLExceptions). Daca am folosi semnaturi fara throws am determina clientul sa trateze exceptiile, la implementare. Am pierde astfel abstractia. Prin folosirea exceptiilor wrapper ii dam clientului posibilitatea de a stii care sunt problemele aparute la rulare.
In versiunile anterioare ale Javei daca aparea o eroare intr-un fir atunci se arunca o eroare exceptie si firul se termina, exceptia navigand spre radacina ThreadGroup. Numele firului, numele exceptiei, mesajul exceptiei si stack trace-ul erau afisate. In 1.4 si versiunile anterioare inseram cod propriu in ThreadGroup pentru a manipula aceste exceptii. In 1.6 manipularea exceptiilor neprinse este simplificata pentru ca putem defini manipulatorul exceptiilor neprinse, la nivelul firului de executie.
Metoda setUncaughtExceptionHandler(), introdusa in 1.5, ne permite sa preluam controlul deplin asupra raspunsului unui fir, in cazul exceptiilor neprinse, prin setarea unui manipulator de exceptii neprinse. Pentru a seta manipulatorul in toate firele vom trimite ca parametru al acestei metode o implementare a interfetei Thread.UncaughtExceptionHandler. Metoda acestei interfete este uncaughtException(). Daca niciun manipulator nu este setat obiectul ThreadGroup va functiona ca un manipulator de exceptie.
Metoda getUncaughtExceptionHandler() returneaza handler-ul invocat atunci cand un fir se termina din cauza unei exceptii neprinse. Pentru a determina cum sa manipuleze exceptia neprinsa, JVM mai intai cauta un manipulator de fire pe care sa-l invoce. Daca exista il invoca. Apoi JVM trimite exceptia in ierarhia ThreadGroup pina ce atinge radacina acesteia, ceea ce inseamna ca ThreadGroup nu a suprascris uncaughtException(). In cele din urma JVM apeleza Thread.getDefaultExceptionHandler() pentru a invoca manipulatorul predefinit.
In exemplul urmator vom crea un manipulator de exceptie, HandlerPropriu, pe care il vom seta intr-un fir, ExceptieNeprinsa. Metoda run() a acestui fir nu prinde nicio exceptie, desi face o impartire la zero. Exceptia este preluata automat de catre manipulatorul definit.
class HandlerPropriu implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.err.printf("%s: %s la linia %d in fisierul %s%n", t.getName(), e.toString(), e.getStackTrace()[0].getLineNumber(), e.getStackTrace()[0].getFileName());
}
}
public class ExceptieNeprinsa extends Thread{
public ExceptieNeprinsa() {
setName("Exceptia neprinsa explicit:") ;
setUncaughtExceptionHandler(new HandlerPropriu()) ;
}
public void run(){
System.out.println(3/0);
}
public static void main (String args[]) {
ExceptieNeprinsa x=new ExceptieNeprinsa();
x.start();
}
}
Asertiuni (assertions)
Asertiunea ne permite sa testam presupunerea despre cum ruleaza un program. Asertiunea contine o expresie booleana, avand valoarea predefinita true. Asertiunea este diferita de toate celelalte teste pentru ca verifica conditii ce nu pot fi violate. Daca expresia booleana are valoarea false atunci sistemul arunca o AssertionError. Mecanismul de asertiuni este valoros deoarece ofera debugging mult mai eficient, ajuta la intretinerea codului prin documentarea lucrului intern al programului, incurajeaza forma de programare contractuala (contract programming).
Programarea contractuala este o tehnica utilizata pentru a incuraja un inalt nivel al calitatii softului, reutilizarii si increderii. Furnizorul si consumatorul unui apel de metoda sunt impreuna de acord sa adere la un contract bine definit, ce este fortat de folosirea asertiunilor. Asertiunile sunt in mod predefinit dezactivate. Putem sa le activam sau dezactivam folosind comenzi la rulare. Asertiunile asigura ca o regula stabilita in cod nu poate fi niciodata modificata. Putem folosi asertiunile ca pe o masura de siguranta, pentru a confirma presupunerile despre comportamentul unui program.
Exista doua forme de asertiuni:
-
simpla, ce poate fi exprimata ca: assert Expresie; unde Expresie este o expresie logica. Cand asertiunile sunt activate, sistemul ruleaza asertiunile si evalueaza expresia din ele. Daca aceasta are valoarea false atunci se va arunca o AssertionError, fara niciun mesaj
-
complexa, ce poate fi exprimata ca: assert Expresie1:Expresie2;. Aceasta se foloseste pentru a furniza un mesaj complex in caz de eroare. Expresie1 este booleana si va fi evaluata. In caz de eroare valoarea lui Expresie2 va fi trimisa constructorului lui AssertionError si un mesaj de eroare detaliat va fi afisat.
AssertionError este o eroare ce opreste executia programului si reprezinta o subclasa a java.lang.Error. Aceasta eroare nu poate fi prinsa.
Nu vom folosi asertiunile pentru:
-
verificarea argumentelor metodelor: metodele publice au un contract clar definit cu utilizatorii, care nu depinde de faptul ca asertiunile sunt activate. In locul asertiunile pentru argumente ilegale se poate folosi IllegalArgumentException.
-
efectuarea functiilor critice cerute de aplicatie. Aceasta problema apare atunci cand asertiunile sunt dezactivate
-
producerea efectelor secundare. Acesta problema apare atunci cand asertiunile sunt dezactivate
Urmatorul exemplu ilustreaza de ce nu putem folosi asertiunile pentru executarea operatiilor critice
assert carNames.remove(null);
In cazul in care asertiunile sunt dezactivate metoda remove() nu se va executa, ceea ce ar produce rezultate inconsitente. Solutia de rezolvare consistenta a situatiei este urmatoarea:
boolean nullsRemoved = carNames.remove( null );
assert nullsRemoved;
Asertiunile pot fi folosite pentru testarea:
-
invariantilor interni: un invariant intern este o conditie ce trebuie sa fie true tot timpul. Asertiunile se folosesc ori de cate ori facem o presupunem asupra unui invariant. O folosim in if-uri multiple, pe ultima ramura else sau in switch pe ramura default. Spre exemplu:
if (j % 5 == 0) {
...
} else if (j % 5 == 1) {
...
} else {
assert j % 5 == 2 : "presupunerea este incorecta";
....
}
-
invariantilor de control ai fluxului: pentru aceasta plasam asertiunea intr-o locatie de cod in care presupunem ca nu putem ajunge. Spre exemplu:
void metoda() {
for (...) {
if (...)
return;
}
assert false; // acest punct al executiei nu ar trebui atins
}
De obicei se foloseste assert false;
-
Preconditiilor, postconditiilor si invariantilor clasei. O preconditie este o conditie pe care o presupunem adevarata la inceputul metodei. Putem testa presupunerea intr-o metoda nepublica, la inceputul acesteia
private void metoda(int param) {
assert ((param >= 50) && (param <= 500)) : param;
...
}
Putem folosi asertiunile pentru a testa postconditiile atat in metode publice cat si nepublice. Putem testa o post conditie chiar inainte de instructiunea return. Pentru aceasta insa trebuie sa ne asiguram ca exista o singura iesire din metoda. Un invariant de clasa este o conditie ce trebuie sa fie adevarata pentru fiecare instanta a clasei. Presupunem ca avem o metoda booleana privata numita IsCorrect() care trebuie sa returneze true inainte si dupa ce orice metoda s-a incheiat. In cod adaugam o asertiune inainte de instructiunea return pentru fiecare metoda publica si constructor. In acest moment testul devine un invariant.
Folosim comenzi in linia de comanda pentru a activa asertiunile. Putem activa asertiunile pentru o aplicatie, un pachet sau o clasa. Sintaxa generala este:
java [-enableassertions | -ea] [:package name"..." | :class name]
Comanda urmatoare activeaza asertiunile aplicatiei Application:
java –ea Application
Pentru o clasa folosim comanda:
java –ea:com.vehicle.motorcar.YourClass
iar pentru un pachet:
java –ea:com.vehicle.motorcar…
cele trei puncte din finalul comenzii precizeaza ca asertiunile sunt active pentru pachetul motorcar si pentru toate subpachetele sale.
Unele clase nu sunt incarcate de class loader ci de JVM. De aceea folosim in completarea comenzii de activare a asertiunilor clauza -eas sau –enablesystemassertions. Spre exemplu:
java –esa –ea:com.vehicle.motorcar.YourClass
Dezactivarea asertiunilor se face folosind clauza –da sau –disableassertions.
Cuvantul rezervat assertion nu poate fi folosit cu versiunile mai vechi ale Javei. Compilarea cu versiunea 1.3 va genera o eroare sau un avertisment. In rest compilarea se face cu: javac –source 1.x fisier, unde x este 4, 5 sau 6.
Vom exemplifica utilizarea asertiunilor folosind frameworkul JUnit 4 din Eclipse. Vom crea mai intai o clasa a carei metoda dorim sa o testam.
public class Asertiuni {
private final String[] ZILELE_SAPTAMANII = {"Luni", "Marti", "Miercuri", "Joi", "Vineri", "Sambata", "Duminica"} ;
public String numeleZilei (int zi) {
assert ((zi >= 1) & (zi <= 7)) : "In afara domeniului:" + zi ;
return ZILELE_SAPTAMANII[zi-1] ;
}
}
Metoda numeleZilei() contine o asertiune ce verifica domeniul indicelui. Asertiunea va trimite si un mesaj in caz de eroare customizat. Vom crea o unitate de testare folosind JUnit Test Case-ul Eclipse, numit Test1, in pachetul Testare, avand Class under test clasa anterioara. Putem selecta, in asistent, metodele pe care dorim sa le supunem testarii.
import static org.junit.Assert.*;
import org.junit.Test;
public class Test1 {
@Test
public final void testMain() {
Asertiuni a=new Asertiuni();
a.numeleZilei(8);
}
}
Observatie: daca folosim asertiuni (ca in cazul exemplului nostru) din Window>Preferences>Java>JUnit selectam checkbox-ul Add ‘-ea’….
Rulam apoi aplicatia ca pe un JunitTest. Un API complet gasim in org.junit.Assert.
JUnit (more) in Detail Static imports with Eclipse
JUnit uses a lot of static methods and Eclipse cannot automatically import static imports. You can make the JUnit test methods available via the content assists.
Open the Preferences via Window -> Preferences and select Java > Editor > Content Assist > Favorites. Add then via "New Member" the methods you need. For example this makes the assertTrue, assertFalse and assertEquals method available.
You can now use Content Assist (Ctrl+Space) to add the method and the import.
I suggest to add at least the following new members.
-
org.junit.Assert.assertTrue
-
org.junit.Assert.assertFalse
-
org.junit.Assert.assertEquals
-
org.junit.Assert.fail
Annotations
The following give an overview of the available annotations in JUnit 4.x
Table 1. Annotations
Annotation
|
Description
|
@Test public void method()
|
Annotation @Test identifies that this method is a test method.
|
@Before public void method()
|
Will perform the method() before each test. This method can prepare the test environment, e.g. read input data, initialize the class)
|
@After public void method()
|
Test method must start with test
|
@BeforeClass public void method()
|
Will perform the method before the start of all tests. This can be used to perform time intensive activities for example be used to connect to a database
|
@AfterClass public void method()
|
Will perform the method after all tests have finished. This can be used to perform clean-up activities for example be used to disconnect to a database
|
@Ignore
|
Will ignore the test method, e.g. useful if the underlying code has been changed and the test has not yet been adapted or if the runtime of this test is just to long to be included.
|
@Test(expected=IllegalArgumentException.class)
|
Tests if the method throws the named exception
|
@Test(timeout=100)
|
Fails if the method takes longer then 100 milliseconds
|
Assert statements
The following gives an overview of the available test methods:
Table 2. Test methods
Statement
|
Description
|
fail(String)
|
Let the method fail, might be usable to check that a certain part of the code is not reached.
|
assertTrue(true);
|
True
|
assertsEquals([String message], expected, actual)
|
Test if the values are the same. Note: for arrays the reference is checked not the content of the arrays
|
assertsEquals([String message], expected, actual, tolerance)
|
Usage for float and double; the tolerance are the number of decimals which must be the same
|
assertNull([message], object)
|
Checks if the object is null
|
assertNotNull([message], object)
|
Check if the object is not null
|
assertSame([String], expected, actual)
|
Check if both variables refer to the same object
|
assertNotSame([String], expected, actual)
|
Check that both variables refer not to the same object
|
assertTrue([message], boolean condition)
|
Check if the boolean condition is true.
|
Dostları ilə paylaş: |