Testarea unitară a claselor Java folosind utilitarul JUnit

Testarea unitară s-a impus în ultima perioadă în dezvoltarea proiectelor scrise în limbajul Java și numai, pe măsura apariției unor utilitare gratuite de testare a claselor, care au contribuit la creșterea vitezei de programare și la micșorarea drastică a numărului de bug-uri.

Cel mai folosit utilitar pentru testarea unitară a claselor Java este JUnit, care se poate descărca gratuit de pe site-ul http://www.junit.org . Arhiva este destul de mică și include un director (junitxxx) cu documentație (în directorul doc), documentația API (în directorul javadoc), biblioteca de clase junit.jar și exemple de clase de test (în directorul junit).

Printre avantajele folosirii utilitarului JUnit se numără:

JUnit a fost proiectat pe baza a două modele (patterns): modelul Command si modelul Composite.

O clasa TestCase este un obiect command și orice clasă ce conține metode de test trebuie sa subclaseze clasa TestCase. O clasa TestCase se compune dintr-un numar de metode publice testXXX(). Pentru a verifica rezultatele așteptate și cele curente se va invoca una dintre metodele assert().

Metodele setUp() si tearDown() servesc la initializarea si distrugerea oricărui obiect utilizat în cadrul testelor. Fiecare test rulează într-un context propriu și apelează metoda setUp() înainte și metoda tearDown() după fiecare metodă de test pentru a evita efectele secundare dintre teste.

Instanțele TestCase se pot compune sub forma unor ierarhii TestSuite ce vor invoca automat toate metodele testXXX() definite în fiecare instanță TestCase. O clasă TestSuite poate fi compusă din alte teste, instanțe TestCase sau alte instanțe TestSuite.

Diagrama de clase a pachetului junit.framework este următoarea:

JUnit Framework

Să considerăm ca exemplu o aplicație de tip librărie virtuală, în care se definește o clasă Book ce păstrează informații despre cărțile din librărie și o clasă de tip coș de cumpărături ShoppingCart ce păstrează lista cărților pe care un client dorește să le cumpere.

Codul sursă al celor două clase este prezentat mai jos:

Book.java
/**
 * Implement a Book.
 *
 *@created  04 aprilie 2005
 */
public class Book {
		/**
		 *  The book title.
		 */
		private String title;
		/**
		 *  The book author.
		 */
		private String author;
		/**
		 *  The book price.
		 */
		private double price;

		/**
		 *  Constructor pentru Book.
		 *
		 *@param  title The book title.
		 *@param  author The book author.
		 *@param  price The book price.
		 */
		public Book(String title, String author, double price) {
				this.title = title;
				this.author = author;
				this.price = price;
		}


		/**
		 *  Get book title.
		 *
		 *@return  The book title.
		 */
		public String getTitle() {
				return title;
		}


		/**
		 *  Get book author.
		 *
		 *@return  The book author.
		 */
		public String getAuthor() {
				return author;
		}


		/**
		 *  Get book price.
		 *
		 *@return  The book price.
		 */
		public double getPrice() {
				return price;
		}


		/**
		 *  Test if two Book objects are equals.
		 *
		 *@param  obj The object
		 *@return  true if object are equals.
		 */
		public boolean equals(Object obj) {
				if (obj instanceof Book) {
						Book b = (Book) obj;
						return b.getTitle().equals(title);
				}
				return false;
		}
}

ShoppingCart.java

import java.util.*;
/**
 *  Implement an shopping cart.
 *
 *@created  04 aprilie 2005
 */
public class ShoppingCart {
		/**
		 *  The items list.
		 */
		private List items;


		/**
		 *  The ShoppingCart constructor.
		 */
		public ShoppingCart() {
				items = new ArrayList();
		}


		/**
		 *  Get total price of items from shopping cart.
		 *
		 *@return  The total price.
		 */
		public double getTotal() {
				Iterator i = items.iterator();
				double total = 0.00;
				while (i.hasNext()) {
						Book book = (Book) i.next();
						total = total + book.getPrice();
				}
				return total;
		}


		/**
		 *  Add an item to shopping cart.
		 *
		 *@param  book The book to be added.
		 */
		public void addItem(Book book) {
				items.add(book);
		}


		/**
		 *  Remove an item from shopping cart.
		 *
		 *@param  book The book to be removed.
		 *@throws  Exception If book not found to shopping cart.
		 */
		public void removeItem(Book book) throws Exception {
				if (!items.remove(book)) {
						throw new Exception("Book not found.");
				}
		}


		/**
		 *  Get items count from shopping cart.
		 *
		 *@return  The item count.
		 */
		public int getItemCount() {
				return items.size();
		}


		/**
		 *  Empty shopping cart.
		 */
		public void empty() {
				items.clear();
		}


		/**
		 *  Check if shopping cart is empty.
		 *
		 *@return  true if cart is empty; false otherwhise.
		 */
		public boolean isEmpty() {
				return (items.size() == 0);
		}
}

	

Etapele creării unei clase de test sunt:

Un exemplu de clasă de test pentru coșul de cumparături este prezentat mai jos:


ShoppingCartTest.java
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
 *  Clasa ShoppingCartTest este un exemplu de clasa TestCase.
 *
 *@created  04 aprilie 2005
 */
public class ShoppingCartTest extends TestCase {
		/**
		 *  Obiect de tipul ShoppingCart ce va fi testat.
		 */
		private ShoppingCart bookCart;
		/**
		 *  Obiect de tipul Book ce va fi testat.
		 */
		private Book book;


		/**
		 *  Constructor pentru o instanta ShoppingCartTest cu numele
		 *  specificat.
		 *
		 *@param  name Nume test.
		 */
		public ShoppingCartTest(String name) {
				super(name);
		}


		/**
		 *  Initializare obiecte de test. Se apeleaza inaintea fiecarei metode de
		 *  test.
		 */
		protected void setUp() {
				bookCart = new ShoppingCart();
				book = new Book("Unit testing", "John Smith", 35.95);
				bookCart.addItem(book);
		}


		/**
		 *  Distruge obiectele testate. Se apeleaza dupa fiecare metoda de test.
		 */
		protected void tearDown() {
				bookCart = null;
		}


		/**
		 *  Testare inserare carti in cos.
		 */
		public void testBookAdd() {
				Book book = new Book("Extreme machines", "Thomas Manner", 46.95);
				bookCart.addItem(book);
				double expectedTotalPrice = this.book.getPrice() + book.getPrice();
				assertEquals(expectedTotalPrice, bookCart.getTotal(), 0.0);
				assertEquals(2, bookCart.getItemCount());
		}


		/**
		 *  Test golire cos.
		 */
		public void testEmpty() {
				bookCart.empty();
				assertTrue(bookCart.isEmpty());
		}


		/**
		 *  Testare scoatere carti din cos.
		 *
		 *@throws  Exception Daca nu exista acea carte in cos.
		 */
		public void testBookRemove() throws Exception {
				bookCart.removeItem(book);
				assertEquals(0, bookCart.getItemCount());
				assertEquals(0.0, bookCart.getTotal(), 0.0);
		}


		/**
		 *  Testare scoatere carte inexistenta din cos.
		 */
		public void testBookNotFound() {
				try {
					Book book = new Book("Toy story", "John Scott", 54.95);
					bookCart.removeItem(book);
					fail("Trebuie sa semnaleze o exceptie");
				} catch (Exception w) {
				}
		}


		/**
		 *  Asambleaza si returneaza o suita de teste pentru toate metodele de test
		 *  din calasa de test
		 *
		 *@return  O clasa Test nenula.
		 */
		public static Test suite() {
//
// Adaugarea tuturor metodelor de forma testXXX()
// la suita folosind Java Reflection
//
      TestSuite suite = new TestSuite(ShoppingCartTest.class);
//
// Alternativ, se poate adauga cate o metoda de test...
//
// TestSuite suite = new TestSuite();
// suite.addTest(new ShoppingCartTest("testBookAdd"));
// suite.addTest(new ShoppingCartTest("testEmpty"));
// suite.addTest(new ShoppingCartTest("testBookRemove"));
// suite.addTest(new ShoppingCartTest("testBookNotFound"));
//
				return suite;
		}


		/**
		 *  The Main method.
		 *
		 *@param  args Argumentele din linia de comanda.
		 */
		public static void main(String args[]) {
				junit.textui.TestRunner.run(suite());
				junit.swingui.TestRunner.run(ShoppingCartTest.class);
		}
}

	

Se observă că clasa ShoppingCartTest este extinsă din clasa TestCase. S-a inițializat în metoda setUp() un obiect bookCart de clasă ShoppingCart și un obiect book de clasă Book, care a fost adăugat obiectului bookCart.

În metoda testBookAdd() s-a testat adăugarea unei cărți în coș folosind metoda assertEquals(double expected, double actual, double delta), delta fiind diferenta acceptată pentru comparații intre două valori de tip double, precum și incrementarea numărătorului de itemuri din coș cu metoda assertEquals(int expected, int actual)

Metoda testEmpty() testează golirea coșului după apelul metodei empty(). S-a utilizat metoda assertTrue(boolean condition) care afișează un mesaj de eroare în cazul în care condiția este falsă.

Metoda testBookRemove() testează operația de eliminare a unui obiect Book din coșul bookCart. Coșul are inițial un obiect Book (inserat prin metoda setUp()) care este eliminat din coș cu metoda removeItem(). Se testează ca numărul de obiecte Book rămase să fie zero.

Metoda testBookNotFound() testează inexistența in coș a unui obiect Book care nu a fost adăugat anterior, în caz de eroare se aruncă o excepție prin apelul metodei fail().

Pentru testare se adaugă la CLASSPATH calea către biblioteca junit.jar și către directorul unde se află clasele sursă, se compilează toate cele trei clase si se lansează în execuție cu comanda:

java ShoppingCartTest (din directorul claselor)

Rezultatul la consolă este prezentat în imaginea următoare:

JUnit Console Run

JUnit furnizează atât o interfață text pentru utilizator, cât și o interfață grafică, fiecare indicând numarul de teste, erorile și starea finală a testului.

Interfata text (junit.textui.TestRunner) afișează câte un punct pentru fiecare test efectuat și un OK dacă toate testele au fost trecute cu succes sau mesaje de eroare dacă unul dintre teste a eșuat.

Interfata grafică (junit.swingui.TestRunner) afișează o fereastră de tip Swing cu un bară verde dacă toate testele au trecut cu succes sau o bară roșie dacă unul sau mai multe teste eșuează.

Interfața grafică este specificată în metoda main().

Pentru a afișa rezultatele testelor folosind interfața grafică, în metoda main() în loc de
junit.textui.TestRunner.run(suite());
se scrie
junit.swingui.TestRunner.run(ShoppingCartTest.class);

Se recompilează clasa de test si se lansează în execuție, rezultatul afișat fiind prezentat în imaginea următoare:

JUnit Swing Run

Dacă un anumit test eșuează (de exemplu operația de eliminare a unui obiect Book din ShoppingCart), atunci se va afișa un mesaj de eroare de forma:

JUnit Swing Run Fail

Metodele assertXXX() utilizate la scrierea testelor sunt prezentate în tabelul de mai jos:

assertEquals(primitive expected, primitive actual) Verifică dacă cele două obiecte primitive sunt egale
assertEquals(Object expected,Object actual) Verifică dacă cele două obiecte sunt egale (folosind metoda equals() din Object)
assertSame(Object expected,Object actual) Verifică dacă cele două obiecte au aceeași adresă de memorie
assertNotSame(Object expected,Object actual) Verifică dacă cele două obiecte nu sunt unul și același (nu au aceeași adresă de memorie)
assertNull(Object object) Verifică dacă un obiect este null
assertNotNull(Object object) Verifică dacă un obiect nu este null
assertTrue(boolean condition) Verifică dacă o condiție este adevărată
assertFalse(boolean condition) Verifică dacă o condiție nu este adevărată

Se pot crea suite de test ce include o ierarhie de clase de test sau alte suite. Etapele creării unei suite de test sunt:

Un exemplu de clasă suită de test este următorul:

AllTests.java
import junit.framework.Test;
import junit.framework.TestSuite;
/**
 *  Run all tests.
 *
 *@author  Anonymous
 *@created  04 aprilie 2005
 */
public class AllTests {
		/**
		 *  Asambleaza si returneaza o suita de teste Se pot adauga teste noi.
		 *
		 *@return  O suita de teste.
		 */
		public static Test suite() {
				TestSuite suite = new TestSuite();

				// clasa ShoppingCartTest.
				suite.addTest(ShoppingCartTest.suite());

				// O alta clasa de test.
				//suite.addTest(CreditCardTest.suite());
				return suite;
		}


		/**
		 *  Lanseaza in executie suita de teste.
		 *
		 *@param  args Description of the Parameter
		 */
		public static void main(String args[]) {
				junit.swingui.TestRunner.run(AllTests.class);
		}
}
	

Pentru testarea suitei se compilează toate clasele și se lansează comanda
java AllTests

Testarea automată folosind utilitarul Ant

Dacă proiectul de implementat este construit folosind utilitarul Ant (http://ant.apache.org/), atunci în fișierul build.xml se pot insera teste unitare.

Să presupunem că sursele se află în directorul src, clasele generate în directorul classes, iar biblioteca junit.jar se află în directorul lib al proiectului. Se pot compila sursele Java și se pot rula testele folosind fișierul build.xml, plasat în directorul rădăcină al proiectului. Biblioteca junit.jar trebuie să se găsească în directorul lib al directorului unde este instalat Ant.

Conținutul fișierului build.xml este prezentat mai jos:
build.xml
<?xml version="1.0" encoding="iso-8859-1"?>
<project name="sample-junit" default="all" basedir=".">
    <description>Build Sample JUnit project</description>
    
    <property name="src" value="src"/>
    <property name="lib" value="lib"/>
    <property name="tmp" value="tmp"/>
    
    <path id="cp">
        <fileset dir="${lib}" includes="*.jar"/>
        <pathelement path="${tmp}"/>
    </path>
    
    <target name="compile" description="Compile Java source files">
        <javac srcdir="${src}" destdir="${tmp}" debug="on" deprecation="on" classpathref="cp"/>
    </target>
    
    <target name="test" depends="compile" description="Run JUnit tests">
        <junit haltonfailure="false" haltonerror="false" printsummary="withOutAndErr">
            <classpath refid="cp"/>
            <batchtest>
                <fileset dir="${src}" includes="**/*Test*.java"/>
            </batchtest>
        </junit>
    </target>
    
    <target name="clean" description="Clean generated files">
        <delete dir="${tmp}"/>
        <mkdir dir="${tmp}"/>
    </target>
    
    <target name="all" depends="clean,test" description="Build the whole project"/>
</project>
	

Ant poate afișa rapoarte de test în format HTML. Pentru aceasta trebuie înlocuit targetul de test cu cel de mai jos:

  		<target name="test" depends="compile" description="Run JUnit tests">
        <junit haltonfailure="false" printsummary="withOutAndErr">
            <classpath refid="cp"/>
            <batchtest todir="${tmp}">
                <fileset dir="${src}" includes="**/*Test*.java"/>
            </batchtest>
            <formatter type="xml"/>
        </junit>
        <junitreport todir="${tmp}">
            <fileset dir="${tmp}" includes="TEST-*.xml"/>
            <report format="frames" todir="${tmp}"/>
        </junitreport>
	

Ant va genera un raport XML în directorul tmp, iar elementul junitreport va genera un raport HTML din fișierele XML. Elementul junitreport necesită instalarea bibliotectii Xalan versiunea 2 în directorul lib al instanței Ant. Raportul generat arată ca în imaginea următoare:

JUnit Ant Report

Acest gen de raport este util dacă se rulează un număr mare de teste simultan.

Recomandari

Câteva recomandări cu privire la organizarea și execuția testelor:

Concluzie

JUnit este un utilitar valoros oricărui programator Java, permițând creșterea vitezei de programare și depistarea bug-urilor încă în faza de implementare a codului sursă.

Mai multe unelte de dezvoltare pentru Java (cum ar fi JBuilder sau Eclipse) au încorporat suport pentru JUnit și Ant.

Pentru mai multe informații vă invit sa vizitați site-ul web http://www.junit.org ce conține multe articole și documentație despre JUnit. Cei care lucrează și cu alte limbaje de programare pot folosi alte utilitare de testare unitară ce se pot descărca gratuit de pe Internet:

Referințe

Sursele programelor prezentate: market.zip
JUnit - site oficial: http://www.junit.org
JUnit Primer: http://www.clarkware.com/articles/JUnitPrimer.html
Ant - site oficial: http://www.clarkware.com/articles/JUnitPrimer.html
Refactoring: Improving The Design Of Existing Code, de Martin Fowler (Addison-Wesley, 1999)