L'objectiu principal d'aquest exercici és posar en pràctica la creació de proves unitàries
i d'integració utilitzant objectes simulats amb la llibreria Mockito
i bases de dades incrustades H2
en una aplicació amb arquitectura per capes.
Se'ns ha demanat crear una aplicació bancària per gestionar els comptes bancaris dels clients.
La nostra tasca consistirà en provar la funcionalitat de la implementació de les diferents capes
mitjançant objectes simulats utilitzant Mockito.
Les proves que heu de fer són les següents:
Proves unitàries de la implementció de la capa de servei, utilitzant objectes simulats amb Mockito per la capa de repositori.
Proves unitàries de la implementció de la capa de repositori, utilitzant objectes simulats amb Mockito per la capa de DAO.
Proves unitàries de la implementció de la capa de DAO, utilitzant una base de dades incrustada H2.
Proves d'integració de la implementció de la capa de servei fins a la capa de DAO, utilitzant una base de dades incrustada H2.
packageud8.exercises.bank.domain.service.impl;importud8.common.exception.ResourceNotFoundException;importud8.exercises.bank.domain.entity.BankAccount;importud8.exercises.bank.domain.service.BankAccountService;importud8.exercises.bank.domain.service.NotificationService;importud8.exercises.bank.persistance.repository.BankAccountRepository;publicclassBankAccountServiceImplimplementsBankAccountService{privatefinalBankAccountRepositoryrepository;privatefinalNotificationServicenotificationService;publicBankAccountServiceImpl(BankAccountRepositoryrepository,NotificationServicenotificationService){this.repository=repository;this.notificationService=notificationService;}@OverridepublicBankAccountfindByIBAN(Stringiban)throwsResourceNotFoundException{BankAccountbank=repository.findByIBAN(iban);if(bank==null)thrownewResourceNotFoundException("Bank account not found");returnbank;}@OverridepublicBankAccountcreate(){StringnewIBAN=generateIBAN();BankAccountaccount=newBankAccount(newIBAN);repository.save(account);notificationService.sendNotification(account,"Your new bank account has been created!");returnaccount;}publicStringgenerateIBAN(){BankAccountlatest=repository.latest();intaccountNumber=latest==null?1:latest.getAccountNumber()+1;returnString.format("ES%0"+BankAccount.ACCOUNT_NUMBER_LENGTH+"d",accountNumber);}@Overridepublicbooleanupdate(BankAccountbank){if(!repository.existsByIBAN(bank.getIban())){returnfalse;}repository.save(bank);returntrue;}@Overridepublicbooleandelete(Stringiban){if(!repository.existsByIBAN(iban)){returnfalse;}repository.delete(iban);returntrue;}@Overridepublicbooleandeposit(BankAccountaccount,doubleamount){if(amount<=0){returnfalse;}account.setBalance(account.getBalance()+amount);repository.save(account);returntrue;}@Overridepublicbooleanwithdraw(BankAccountaccount,doubleamount){if(amount<=0||account.getBalance()<amount){returnfalse;}account.setBalance(account.getBalance()-amount);repository.save(account);returntrue;}@Overridepublicbooleantransfer(BankAccountfrom,BankAccountto,doubleamount){if(amount<=0||from.getBalance()<amount){returnfalse;}from.setBalance(from.getBalance()-amount);to.setBalance(to.getBalance()+amount);repository.save(from);repository.save(to);notificationService.sendNotification(from,"Transfer of "+amount+" to "+to.getIban());notificationService.sendNotification(to,"Transfer of "+amount+" from "+from.getIban());returntrue;}}
packageud8.exercises.bank.persistance.dao.impl;importud8.exercises.bank.domain.entity.BankAccount;importud8.exercises.bank.persistance.dao.BankAccountDao;importud8.exercises.bank.persistance.rowmapper.BankAccountRowMapper;importud8.persistance.database.DatabaseConnection;importjava.sql.PreparedStatement;importjava.sql.ResultSet;importjava.sql.SQLException;publicclassBankAccountDaoJdbcimplementsBankAccountDao{privatefinalDatabaseConnectionconnection;privatefinalBankAccountRowMapperbankAccountRowMapper;publicBankAccountDaoJdbc(){this.connection=DatabaseConnection.getInstance();this.bankAccountRowMapper=newBankAccountRowMapper();}@OverridepublicBankAccountfindByIBAN(Stringiban){Stringsql="SELECT * FROM bank_account WHERE iban = ?";try(PreparedStatementpreparedStatement=connection.prepareStatement(sql)){preparedStatement.setString(1,iban);ResultSetrs=preparedStatement.executeQuery();if(!rs.next())returnnull;returnbankAccountRowMapper.mapItem(rs);}catch(SQLExceptione){thrownewRuntimeException(e.getMessage());}}@OverridepublicBankAccountlatest(){Stringsql="SELECT iban, balance FROM bank_account ORDER BY iban DESC LIMIT 1";try(PreparedStatementpreparedStatement=connection.prepareStatement(sql)){ResultSetrs=preparedStatement.executeQuery();if(!rs.next())returnnull;returnbankAccountRowMapper.mapItem(rs);}catch(SQLExceptione){thrownewRuntimeException(e.getMessage());}}@Overridepublicvoidinsert(BankAccountbank){Stringsql="INSERT INTO bank_account (iban, balance) VALUES (?, ?)";try(PreparedStatementpreparedStatement=connection.prepareStatement(sql)){preparedStatement.setString(1,bank.getIban());preparedStatement.setDouble(2,bank.getBalance());preparedStatement.executeUpdate();}catch(SQLExceptione){thrownewRuntimeException(e.getMessage());}}@Overridepublicvoidupdate(BankAccountbank){Stringsql="UPDATE bank_account SET balance = ? WHERE iban = ?";try(PreparedStatementpreparedStatement=connection.prepareStatement(sql)){preparedStatement.setDouble(1,bank.getBalance());preparedStatement.setString(2,bank.getIban());preparedStatement.executeUpdate();}catch(SQLExceptione){thrownewRuntimeException(e.getMessage());}}@Overridepublicvoiddelete(Stringiban){Stringsql="DELETE FROM bank_account WHERE iban = ?";try(PreparedStatementpreparedStatement=connection.prepareStatement(sql)){preparedStatement.setString(1,iban);preparedStatement.executeUpdate();}catch(SQLExceptione){thrownewRuntimeException(e.getMessage());}}}
Aquestes classes són comunes als exemples i exercicis d'aquesta unitat.
DatabaseConnection.java
packageud8.persistance.database;importlombok.extern.log4j.Log4j2;importud8.common.utils.AppPropertiesReader;importjava.io.IOException;importjava.io.InputStream;importjava.io.InputStreamReader;importjava.sql.Connection;importjava.sql.DriverManager;importjava.sql.PreparedStatement;importjava.sql.SQLException;@Log4j2publicclassDatabaseConnection{// Singleton patternprivatestaticDatabaseConnectioninstance=null;publicstaticDatabaseConnectiongetInstance(){if(instance==null){instance=newDatabaseConnection();}returninstance;}privatefinalStringdbUrl;privatefinalStringdbUser;privatefinalStringdbPassword;privatefinalConnectionconnection;privateDatabaseConnection(){dbUrl=AppPropertiesReader.getProperty("app.datasource.url");dbUser=AppPropertiesReader.getProperty("app.datasource.username");dbPassword=AppPropertiesReader.getProperty("app.datasource.password");log.debug("Establint la connexió amb la base de dades...");try{connection=DriverManager.getConnection(dbUrl,dbUser,dbPassword);log.debug("Connexió establerta amb èxit amb els paràmetres:");log.debug(this.getParameters());}catch(SQLExceptione){log.error(e.getMessage());thrownewRuntimeException("Connection parameters :\n\n"+getParameters()+"\nOriginal exception message: "+e.getMessage());}}privateStringgetParameters(){returnString.format("url: %s\nUser: %s\nPassword: %s\n",dbUrl,dbUser,dbPassword);}@SuppressWarnings("SqlSourceToSinkFlow")publicPreparedStatementprepareStatement(Stringsql)throwsSQLException{returnconnection.prepareStatement(sql);}publicvoidsetAutoCommit(booleanautoCommit)throwsSQLException{connection.setAutoCommit(autoCommit);}publicvoidrollback()throwsSQLException{connection.rollback();}publicvoidexecuteScript(StringscriptPath){try{ScriptRunnerscriptRunner=newScriptRunner(connection,false,false);InputStreamscriptStream=getClass().getClassLoader().getResourceAsStream(scriptPath);if(scriptStream==null)thrownewRuntimeException("Script not found: "+scriptPath);scriptRunner.runScript(newInputStreamReader(scriptStream));}catch(IOException|SQLExceptione){thrownewRuntimeException(String.format("Error executing script %s:\n %s\n",scriptPath,e.getMessage()));}}}
ScriptRunner.java
packageud8.persistance.database;/* * Added additional null checks when closing the ResultSet and Statements. * * Thanks to pihug12 and Grzegorz Oledzki at stackoverflow.com * http://stackoverflow.com/questions/5332149/jdbc-scriptrunner-java-lang-nullpointerexception?tab=active#tab-top *//* * Modified: Use logWriter in print(Object), JavaDoc comments, correct Typo. *//* * Modified by Pantelis Sopasakis <chvng@mail.ntua.gr> to take care of DELIMITER statements. This way you * can execute scripts that contain some TRIGGER creation code. New version using REGEXPs! Latest * modification: Cater for a NullPointerException while parsing. Date: Feb 16, 2011, 11:48 EET *//* * Slightly modified version of the com.ibatis.common.jdbc.ScriptRunner class from the iBATIS Apache * project. Only removed dependency on Resource class and a constructor *//* * Copyright 2004 Clinton Begin Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing * permissions and limitations under the License. */importjava.io.IOException;importjava.io.LineNumberReader;importjava.io.PrintWriter;importjava.io.Reader;importjava.sql.Connection;importjava.sql.ResultSet;importjava.sql.ResultSetMetaData;importjava.sql.SQLException;importjava.sql.Statement;importjava.util.regex.Matcher;importjava.util.regex.Pattern;/** * Tool to run database scripts. This version of the script can be found at * https://gist.github.com/gists/831762/ */publicclassScriptRunner{privatestaticfinalStringDEFAULT_DELIMITER=";";privatestaticfinalStringDELIMITER_LINE_REGEX="(?i)DELIMITER.+";privatestaticfinalStringDELIMITER_LINE_SPLIT_REGEX="(?i)DELIMITER";privatefinalConnectionconnection;privatefinalbooleanstopOnError;privatefinalbooleanautoCommit;privatePrintWriterlogWriter=newPrintWriter(System.out);privatePrintWritererrorLogWriter=newPrintWriter(System.err);privateStringdelimiter=DEFAULT_DELIMITER;privatebooleanfullLineDelimiter=false;/** * Default constructor. * * @param connection * @param autoCommit * @param stopOnError */publicScriptRunner(Connectionconnection,booleanautoCommit,booleanstopOnError){this.connection=connection;this.autoCommit=autoCommit;this.stopOnError=stopOnError;}/** * @param delimiter * @param fullLineDelimiter */publicvoidsetDelimiter(Stringdelimiter,booleanfullLineDelimiter){this.delimiter=delimiter;this.fullLineDelimiter=fullLineDelimiter;}/** * Setter for logWriter property. * * @param logWriter * - the new value of the logWriter property */publicvoidsetLogWriter(PrintWriterlogWriter){this.logWriter=logWriter;}/** * Setter for errorLogWriter property. * * @param errorLogWriter * - the new value of the errorLogWriter property */publicvoidsetErrorLogWriter(PrintWritererrorLogWriter){this.errorLogWriter=errorLogWriter;}/** * Runs an SQL script (read in using the Reader parameter). * * @param reader * - the source of the script * @throws SQLException * if any SQL errors occur * @throws IOException * if there is an error reading from the Reader */publicvoidrunScript(Readerreader)throwsIOException,SQLException{try{booleanoriginalAutoCommit=connection.getAutoCommit();try{if(originalAutoCommit!=autoCommit){connection.setAutoCommit(autoCommit);}runScript(connection,reader);}finally{connection.setAutoCommit(originalAutoCommit);}}catch(IOExceptione){throwe;}catch(SQLExceptione){throwe;}catch(Exceptione){thrownewRuntimeException("Error running script. Cause: "+e,e);}}/** * Runs an SQL script (read in using the Reader parameter) using the connection passed in. * * @param conn * - the connection to use for the script * @param reader * - the source of the script * @throws SQLException * if any SQL errors occur * @throws IOException * if there is an error reading from the Reader */privatevoidrunScript(Connectionconn,Readerreader)throwsIOException,SQLException{StringBuffercommand=null;try{LineNumberReaderlineReader=newLineNumberReader(reader);Stringline=null;while((line=lineReader.readLine())!=null){if(command==null){command=newStringBuffer();}StringtrimmedLine=line.trim();if(trimmedLine.startsWith("--")){println(trimmedLine);}elseif(trimmedLine.length()<1||trimmedLine.startsWith("//")){// Do nothing}elseif(trimmedLine.length()<1||trimmedLine.startsWith("--")){// Do nothing}elseif(!fullLineDelimiter&&trimmedLine.endsWith(getDelimiter())||fullLineDelimiter&&trimmedLine.equals(getDelimiter())){Patternpattern=Pattern.compile(DELIMITER_LINE_REGEX);Matchermatcher=pattern.matcher(trimmedLine);if(matcher.matches()){setDelimiter(trimmedLine.split(DELIMITER_LINE_SPLIT_REGEX)[1].trim(),fullLineDelimiter);line=lineReader.readLine();if(line==null){break;}trimmedLine=line.trim();}command.append(line.substring(0,line.lastIndexOf(getDelimiter())));command.append(" ");Statementstatement=conn.createStatement();println(command);booleanhasResults=false;if(stopOnError){hasResults=statement.execute(command.toString());}else{try{statement.execute(command.toString());}catch(SQLExceptione){e.fillInStackTrace();printlnError("Error executing: "+command);printlnError(e);}}if(autoCommit&&!conn.getAutoCommit()){conn.commit();}ResultSetrs=statement.getResultSet();if(hasResults&&rs!=null){ResultSetMetaDatamd=rs.getMetaData();intcols=md.getColumnCount();for(inti=0;i<cols;i++){Stringname=md.getColumnLabel(i);print(name+"\t");}println("");while(rs.next()){for(inti=1;i<=cols;i++){Stringvalue=rs.getString(i);print(value+"\t");}println("");}}command=null;try{if(rs!=null){rs.close();}}catch(Exceptione){e.printStackTrace();}try{if(statement!=null){statement.close();}}catch(Exceptione){e.printStackTrace();// Ignore to workaround a bug in Jakarta DBCP}}else{Patternpattern=Pattern.compile(DELIMITER_LINE_REGEX);Matchermatcher=pattern.matcher(trimmedLine);if(matcher.matches()){setDelimiter(trimmedLine.split(DELIMITER_LINE_SPLIT_REGEX)[1].trim(),fullLineDelimiter);line=lineReader.readLine();if(line==null){break;}trimmedLine=line.trim();}command.append(line);command.append(" ");}}if(!autoCommit){conn.commit();}}catch(SQLExceptione){e.fillInStackTrace();printlnError("Error executing: "+command);printlnError(e);throwe;}catch(IOExceptione){e.fillInStackTrace();printlnError("Error executing: "+command);printlnError(e);throwe;}finally{conn.rollback();flush();}}privateStringgetDelimiter(){returndelimiter;}privatevoidprint(Objecto){if(logWriter!=null){logWriter.print(o);}}privatevoidprintln(Objecto){if(logWriter!=null){logWriter.println(o);}}privatevoidprintlnError(Objecto){if(errorLogWriter!=null){errorLogWriter.println(o);}}privatevoidflush(){if(logWriter!=null){logWriter.flush();}if(errorLogWriter!=null){errorLogWriter.flush();}}}
Aquestes classes són comunes als exemples i exercicis d'aquesta unitat.
AppPropertiesReader.java
packageud8.common.utils;importlombok.extern.log4j.Log4j2;importjava.io.IOException;importjava.io.InputStream;importjava.util.Properties;@Log4j2publicclassAppPropertiesReader{privatestaticfinalPropertiesproperties=newProperties();static{loadProperties("application.properties");// Carga las propiedades por defecto// Detectar el perfil y cargar las propiedades correspondientesStringactiveProfile=getProperty("app.profiles.active");if(activeProfile!=null){log.debug("Perfil actiu: "+activeProfile);loadProperties("application-"+activeProfile+".properties");}}privatestaticvoidloadProperties(Stringfilename){try(InputStreaminput=Thread.currentThread().getContextClassLoader().getResourceAsStream(filename)){if(input==null){System.out.println("No s'ha pogut trobar el fitxer de configuració: "+filename);return;}// Cargar las propiedades desde el archivo de configuraciónproperties.load(input);}catch(IOExceptione){System.err.println("Error al llegir el fitxer de configuració: "+filename);}}publicstaticStringgetProperty(Stringkey){returnproperties.getProperty(key);}}