e-Commerce Test First ! Automatisiertes Testen mit dem JUnit Framework Prof. Dr. Nikolaus Wulff JUnit • JUnit ist das Opensource Testframework. • Es existieren Portierungen für fast alle objektorientierten Sprachen: – Smalltalk, C++, Java, C#, etc. • JUnit ist konzipiert als Komponententest in der Hand des Entwicklers, es eignet sich jedoch auch für Funktionale- und Lasttests. • Es gibt Erweiterungen zum Test von Web- und EJB-Anwendungen (HttpUnit, Cactus). • Link: http://www.junit.org e-Commerce 2 Testentwicklung • Zu jeder Klasse im Projekt wird ein passender Test geschrieben, der das Verhalten der jeweiligen Klasse testet. • JUnit definiert das Rahmenwerk für derartige Tests, das in allen gängigen IDE's integriert ist. • Der Entwickler leitet seine Tests von einer JUnit TestCase Klasse ab und implementiert nur noch die speziellen, fachlichen Testmethoden. e-Commerce 3 Alles ist ein Test Package: junit.framework TestRunner runs <<Interface>> 0..* Test JUnit Framework • TestCase MyClass aMethod() • • MyClassTest testAMethod() TestSuite Eigenentwicklung • Der Test ist das zentrale Objekt. Der JUnit TestRunner, führt den Test aus und protokolliert die Ergebnisse. Ein Test ist entweder ein einzelnes TestCase oder eine Ansammlung von Tests zusam-mengefasst in einer TestSuite. Zu jeder Klasse "MyClass" wird eine entsprechende Testklasse "MyClassTest" geschrieben, die die JUnit TestCase Basisklasse erweitert. e-Commerce 4 Testausführung • JUnit sucht per Reflection alle Methoden in der Testklasse, deren Namen mit »test« beginnen und führt diese in einer definierten Sequenz aus: – – – – Konstruktor setUp run: testXXX tearDown Erzeugt das Testobjekt. Initialisiert den Test. Führt den Test durch. Auf- und Abräumen des Tests. • Die Testergebnisse werden gesammelt und visualisiert (GUI) oder protokolliert (Ant). e-Commerce 5 Eclipse JUnit Plugin e-Commerce 6 Testvalidierung • Die Ergebnisse der zu testenden Klasse / Methode werden in jeder Testmethode validiert. • Hierzu werden Bedingungen formuliert, die den Rückgabewert der zu testenden Methode mit einem als wahr bekannten Wert vergleichen. • Dieser Vergleich wird in der JUnit Methode assertEquals oder assertTrue ausgewertet. /** * test the addition operation. */ public void testAdd() throws Exception { double expected = 2 + 4; double result = calculator.calculate(2, 4, '+'); super.assertEquals(expected,result,DELTA); } e-Commerce 7 Das JUnit Framework <<Interface>> TestListener TestRunner (from ui) 0..* runs <<Interface>> Test TestResult 0..* failur e 0..* 0..* run() error 0..* TestCase TestSuite TestFailure setUp() tearDo wn () run() addTest() run() • Das folgende Klassendiagramm zeigt das vollständige JUnit Framework. • Es gibt spezielle Erweiterungen für wiederholte Test oder bestimmte Setups, z.B. Datenbankverbindungen etc. decorate TestDecorator (from extensions) TestSetup RepeatedTest (from extensions) (from extensions) e-Commerce 8 Qualitätssicherung • Stringentes Arbeiten mit JUnit etabliert eine effiziente Qualitätssicherung im Projekt. • Angefangen von einzelnen Klassen lassen sich ganze Testhierarchien bis hin zum gesamten Projekt aufbauen. • Wird dieser Projekttest in die tägliche Master-Build integriert, ist immer bekannt, wie es um das Projekt bestellt ist... • Zugleich verbessert der Test-First Ansatz die Definition von Schnittstellen und Architektur. e-Commerce 9 TestSuite Hierarchie Project ProjectTest 1 1..* 1..* jeder Test ist für sich alleine in JUnit lauffähig... 1..* Package PackageTest 1 TestSuite 1..* 1..* <<Interface>> Test 1..* 1..* Class TestCase ClassTest 1 1..* 1..* 1..* Method testMethode 1 1..* Die Tests folgen der Package-Struktur, haben jedoch im Dateisystem eine eigene Wurzel Test, so daß die Build unabhängig von den TestCases läuft... e-Commerce 10 Was ist zu Testen? • Im Prinzip alles ... :-) • Aufwand und Nutzen abwägen! – Testen muß bezahlbar sein. – Ungetestete Software kann teuer werden ... – Sicherheitsrelevante Software (Luftfahrt, Medzin etc.) stellt andere Ansprüche! • • • • Inkrementelle und iterative Entwicklung => Tests müssen häufig wiederholt werden. => Tests müssen automatisierbar sein. Preisfrage: Wer testet den Test(er)? e-Commerce Testpragmatik • Tests müssen wiederhol- und reproduzierbar sein. – Definierter Anfangs- und Endzustand. – Eindeutigkeit des Testergebnis. • • • • • Möglichst alle Klassen und Methoden testen. Getter und Setter symmetrisch testen. Bereiche abdecken. Linker und rechter Rand! Pre- und PostConditions überprüfen. Sowohl positiv als auch negativ Fälle testen. – => if-else und switch-case werden abgedeckt. • Gezielt Exceptions provozieren. e-Commerce Testpragmatik cont. • Test first Ansatz. – Jede neue Anforderung erfordert einen neuen Test. – Der Test wird vor der Implementierung geschrieben. • Wird ein Bug gefunden, so wird dazu ein Test geschrieben und anschließend der Bug gefixed. – Das Wesentliche wird getestet. • Ein einmal entwickelter Test wird an aktuelle Weiterentwicklungen angepasst und nicht entfernt. • Die Testüberdeckung wächst dynamisch. – Die wesentlichen Klassen werden am meisten getestet. e-Commerce BookStore Test TestCase (from j unit.framew ork) BookStoreTest tests -dri ver BookStore <<uses>> find/store <<Interface>> Driver (from j av a.sql) <<creates>> <<creates>> <<Interface>> Connection Book (from j av a.sql) Book attributs map to the ResultSet <<creates>> <<Interfac e>> ResultSet • Test mit JUnit. • Typische zu testende Operationen sind: (from j av a. sql) – – – – <<creates>> <<Interface>> Statement (from j av a.sql) findAll findByXXX store per update oder insert Löschen per delete e-Commerce 14 Testaufbau public class BookStoreTest extends TestCase { private private private private private final static String DRIVER = "org.gjt.mm.mysql.Driver"; final static String DBURL = "jdbc:mysql://localhost/bookstore" final static String USER = "bookstore"; final static String PWD = "bookstore"; BookStore bookstore; Verwendet wird der MySQL /** Treiber für die bookstore Datenbank. * @param arg0 URL, User und Passwort wie angegeben */ public BookStoreTest(String arg0) { super(arg0); } /** * Setup for the test. */ public void setUp() throws Exception { bookstore = new BookStore(DRIVER, DBURL, USER, PWD); } e-Commerce 15 Test von find und store /** * test the findAll method. * @throws Exception */ public void testFindAll() throws Exception { Collection result = bookstore.findAll(); assertTrue("result empty ", result.size() > 0); } /** * test the insert operation. * @throws Exception */ public void testInsert() throws Exception { String isbn = (new Date()).toString(); Book book = new Book(isbn, "junit", getName(), "JUnit Test"); bookstore.store(book); Collection result = bookstore.findByIsbn(isbn); assertTrue("insert not found ", result.size() == 1); } e-Commerce 16 BookStore Erzeugung public class BookStore { private final Driver driver; private final String url; private final String user; private final String password; /** * Constructor to initialize the BookStore database connection. * * @param driverName String classname of the JDBC driver * @param url String the url of the database * @param user String the user name of the database * @param password String the database password * @throws Exception any kind of exceptions to be thrown... */ public BookStore(String driverName, String url, String user, String password) throws Exception { Class clazz = Class.forName(driverName); this.driver = (Driver) clazz.newInstance(); this.url = url; this.user = user; this.password = password; } e-Commerce 17 Verbindung zur Datenbank /** * Returns a sql connection to the database. * * @return java.sql.Connection the connection to use * @throws java.sql.SQLException in case of an error */ private Connection getConnection() throws SQLException { Properties props = new Properties(); props.setProperty("user", user); props.setProperty("password", password); return driver.connect(url, props); } • Die Verbindung zur Datenbank wird für autorisierte User mit entsprechendem Paßwort erzeugt. • Im Fehlerfall wird eine SQLException geworfen. • Die Ressourcen müssen nach dem Gebrauch per close Befehl wieder freigegeben werden. e-Commerce 18 Resourcenfreigabe private void releaseResources(Connection con, Statement stm, ResultSet rs) { try { if (rs != null) rs.close(); } catch (Exception e) { handleError(e); } try { if (stm != null) stm.close(); } catch (Exception e) { handleError(e); } try { if (con != null) con.close(); } catch (Exception e) { handleError(e); } } e-Commerce 19 Generische Suche public Collection findByAuthor(String author) { String where = " where author='" + author + "'"; return find(where); } public Collection findByIsbn(String isbn) { String where = " where isbn='" + isbn + "'"; return find(where); } public Collection findByTitle(String title) { String where = " where title='" + title + "'"; return find(where); } Alle Suchoperationen verzweigen auf public Collection findAll() { die interne, generische find Operation. return find(""); } Zurückgegeben wird eine Collection gefüllt mit Büchern. e-Commerce 20 Die generische Suchroutine private Collection find(String where) { ResultSet set = null; Statement stm = null; Connection con = null; ArrayList bookList = new ArrayList(); String sql = "select * from books " + where; try { con = getConnection(); stm = con.createStatement(); set = stm.executeQuery(sql); while (set.next()) { long id = set.getLong("id"); String isbn = set.getString("isbn"); String author = set.getString("author"); String title = set.getString("title"); String description = set.getString("description"); bookList .add(new Book(id, isbn, author, title, description)); } } catch (SQLException e) { handleError(e); } finally { releaseResources(con, stm, set); } return bookList ; } e-Commerce 21 Generisches Speichern public void store(Book book) { if (findById(book.id.longValue()).size() == 0) { insert(book); } else { update(book); } } • Je nach dem ob das Buch schon in der Datenbank vorhanden ist oder nicht muss unterschiedlich vorgegangen werden: – Buch ist neu => Insert Statement – Buch ist alt => Update Statement e-Commerce 22 Speichern per Insert private void insert(Book book) { StringBuffer sql = new StringBuffer("insert into books "); sql.append("(isbn,author,title,description) "); sql.append(" values("); sql.append("'"); sql.append(book.isbn); sql.append("',"); sql.append("'"); sql.append(book.author); insert und update Methoden erzeugen sql.append("',"); ein Kommando, das an die executeSQL sql.append("'"); Methode übergeben & ausgeführt wird. sql.append(book.title); sql.append("',"); sql.append("'"); sql.append(book.description); sql.append("'"); sql.append(")"); // forward the sql string to the executeSQL method executeSQL(sql.toString()); } e-Commerce 23 Speichern per Update private void update(Book book) { StringBuffer sql = new StringBuffer("update books "); sql.append(" set isbn='"); sql.append(book.isbn); sql.append("', author='"); sql.append(book.author); sql.append("', title='"); sql.append(book.title); sql.append("', description='"); sql.append(book.description); sql.append("' where id="); sql.append(book.id); // forward the sql string to the executeSQL method executeSQL(sql.toString()); } insert und update Methoden erzeugen ein Kommando, das an die executeSQL Methode übergeben & ausgeführt wird. e-Commerce 24 Löschen per Delete private void delete(Book book) { StringBuffer sql = new StringBuffer("delete from books "); sql.append(" where id="); sql.append(book.id); // forward the sql string to the executeSQL method executeSQL(sql.toString()); } private void executeSQL(String sql) { Connection con = null; Statement stm = null; try { con = getConnection(); stm = con.createStatement(); stm.executeUpdate(sql); } catch (SQLException e) { handleError(e); } finally { releaseResources(con, stm, null); } } e-Commerce 25 Übung • Erstellen Sie eine BookStore Datenbank. • Registrieren Sie die Datenbank als ODBC Quelle. • Implementieren Sie die BookStore Anwendung. – store(Book b) und delete(Book b) – findAll – findByXXX xxx{Author, Isbn,Title, Id} • Testen Sie alle Operationen mit JUnit. • Tip: Nehmen Sie die Quellcodeauszüge als Vorlage. e-Commerce 26