initial version of support project with build support.
[utils] / test / org / wamblee / test / SpringTestCase.java
1 /*
2  * Copyright 2005 the original author or authors.
3  * 
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  * 
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * 
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package org.wamblee.test;
18
19 import java.sql.Connection;
20 import java.sql.PreparedStatement;
21 import java.sql.ResultSet;
22 import java.sql.SQLException;
23 import java.util.ArrayList;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.TreeMap;
28
29 import javax.sql.DataSource;
30
31 import junit.framework.TestCase;
32
33 import org.apache.commons.logging.Log;
34 import org.apache.commons.logging.LogFactory;
35 import org.dbunit.DatabaseUnitException;
36 import org.dbunit.database.DatabaseConnection;
37 import org.dbunit.database.DatabaseSequenceFilter;
38 import org.dbunit.database.IDatabaseConnection;
39 import org.dbunit.dataset.FilteredDataSet;
40 import org.dbunit.dataset.IDataSet;
41 import org.dbunit.dataset.filter.ITableFilter;
42 import org.dbunit.operation.DatabaseOperation;
43 import org.hibernate.SessionFactory;
44 import org.jmock.cglib.MockObjectTestCase;
45 import org.springframework.beans.factory.NoSuchBeanDefinitionException;
46 import org.springframework.beans.factory.config.BeanDefinition;
47 import org.springframework.beans.factory.support.RootBeanDefinition;
48 import org.springframework.context.ApplicationContext;
49 import org.springframework.context.support.ClassPathXmlApplicationContext;
50 import org.springframework.context.support.GenericApplicationContext;
51 import org.springframework.jdbc.core.JdbcTemplate;
52 import org.springframework.jdbc.datasource.DataSourceUtils;
53 import org.springframework.jdbc.datasource.DriverManagerDataSource;
54 import org.springframework.orm.hibernate3.HibernateTemplate;
55 import org.springframework.transaction.PlatformTransactionManager;
56 import org.springframework.transaction.TransactionDefinition;
57 import org.springframework.transaction.TransactionStatus;
58 import org.springframework.transaction.support.DefaultTransactionDefinition;
59 import org.springframework.transaction.support.TransactionCallback;
60 import org.springframework.transaction.support.TransactionCallbackWithoutResult;
61 import org.springframework.transaction.support.TransactionTemplate;
62 import org.wamblee.general.BeanKernel;
63 import org.wamblee.persistence.hibernate.HibernateMappingFiles;
64
65 /**
66  * Test case support class for spring tests. 
67  */
68 public class SpringTestCase extends MockObjectTestCase {
69
70     private Log LOG = LogFactory.getLog(SpringTestCase.class);
71
72     /**
73      * Session factory bean name.
74      */
75     private static final String SESSION_FACTORY = "sessionFactory";
76
77     /**
78      * Data source bean name.
79      */
80     private static final String DATA_SOURCE = "dataSource";
81
82     /**
83      * Transaction manager bean name.
84      */
85     private static final String TRANSACTION_MANAGER = "transactionManager";
86
87     /**
88      * Name of the ConfigFileList bean that describes the Hibernate mapping
89      * files to use.
90      */
91     private static final String HIBERNATE_CONFIG_FILES = "hibernateMappingFiles";
92
93     /**
94      * Schema pattern.
95      */
96     private static final String SCHEMA_PATTERN = "%";
97    
98     /**
99      * List of (String) configuration file locations for spring.
100      */
101     private String[] _configLocations;
102     
103     /**
104      * Application context for storing bean definitions that vary on a test by
105      * test basis and cannot be hardcoded in the spring configuration files.
106      */
107     private GenericApplicationContext _parentContext;
108     
109     /**
110      * Cached spring application context. 
111      */
112     private ApplicationContext _context; 
113
114     public SpringTestCase(Class<? extends SpringConfigFiles> aSpringFiles,
115             Class<? extends HibernateMappingFiles> aMappingFiles) {
116         try {
117             SpringConfigFiles springFiles = aSpringFiles.newInstance();
118             _configLocations = springFiles.toArray(new String[0]);
119         } catch (Exception e) {
120             fail("Could not construct spring config files class '" + aSpringFiles.getName() + "'"); 
121         }
122    
123         // Register the Hibernate mapping files as a bean.
124         _parentContext = new GenericApplicationContext();
125         BeanDefinition lDefinition = new RootBeanDefinition(aMappingFiles);
126         _parentContext.registerBeanDefinition(HIBERNATE_CONFIG_FILES,
127                 lDefinition);
128         _parentContext.refresh();
129
130     }
131
132     /**
133      * Gets the spring context.
134      * 
135      * @return Spring context.
136      */
137     protected synchronized ApplicationContext getSpringContext() {
138         if ( _context == null ) { 
139             _context = new ClassPathXmlApplicationContext(
140                 (String[]) _configLocations, _parentContext);
141             assertNotNull(_context);
142         }
143         return _context; 
144     }
145
146     /**
147      * @return Hibernate session factory.
148      */
149     protected SessionFactory getSessionFactory() {
150         SessionFactory factory = (SessionFactory) getSpringContext().getBean(SESSION_FACTORY);
151         assertNotNull(factory); 
152         return factory; 
153     }
154
155     protected void setUp() throws Exception {
156         LOG.info("Performing setUp()");
157
158         super.setUp();
159         
160         _context = null;  // make sure we get a new application context for every
161                           // new test. 
162
163         BeanKernel
164                 .overrideBeanFactory(new TestSpringBeanFactory(getSpringContext()));
165   
166         cleanDatabase(); 
167     }
168
169     /*
170      * (non-Javadoc)
171      * 
172      * @see junit.framework.TestCase#tearDown()
173      */
174     @Override
175     protected void tearDown() throws Exception {
176         try {
177             super.tearDown();
178         } finally {
179             LOG.info("tearDown() complete");
180         }
181     }
182
183     /**
184      * @return Transaction manager
185      */
186     protected PlatformTransactionManager getTransactionManager() {
187         PlatformTransactionManager manager = (PlatformTransactionManager) getSpringContext()
188         .getBean(TRANSACTION_MANAGER);
189         assertNotNull(manager); 
190         return manager; 
191     }
192
193     /**
194      * @return Starts a new transaction.
195      */
196     protected TransactionStatus getTransaction() {
197         DefaultTransactionDefinition def = new DefaultTransactionDefinition();
198         def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
199
200         return getTransactionManager().getTransaction(def);
201     }
202
203     /**
204      * Returns the hibernate template for executing hibernate-specific
205      * functionality.
206      * 
207      * @return Hibernate template.
208      */
209     protected HibernateTemplate getTemplate() {
210         HibernateTemplate template = (HibernateTemplate) getSpringContext().getBean(HibernateTemplate.class.getName());
211         assertNotNull(template); 
212         return template; 
213     }
214
215     /**
216      * Flushes the session. Should be called after some Hibernate work and
217      * before JDBC is used to check results.
218      * 
219      */
220     protected void flush() {
221         getTemplate().flush();
222     }
223
224     /**
225      * Flushes the session first and then removes all objects from the Session
226      * cache. Should be called after some Hibernate work and before JDBC is used
227      * to check results.
228      * 
229      */
230     protected void clear() {
231         flush();
232         getTemplate().clear();
233     }
234
235     /**
236      * Evicts the object from the session. This is essential for the
237      * implementation of unit tests where first an object is saved and is
238      * retrieved later. By removing the object from the session, Hibernate must
239      * retrieve the object again from the database.
240      * 
241      * @param aObject
242      */
243     protected void evict(Object aObject) {
244         getTemplate().evict(aObject);
245     }
246
247     /**
248      * Gets the connection.
249      * 
250      * @return Connection.
251      */
252     public Connection getConnection() {
253         return DataSourceUtils.getConnection(getDataSource());
254     }
255
256     public void cleanDatabase() throws SQLException {
257         
258         if (! isDatabaseConfigured() ) {
259             return; 
260         }
261         
262         String[] tables = getTableNames();
263
264         try {
265             IDatabaseConnection connection = new DatabaseConnection(
266                     getConnection());
267             ITableFilter filter = new DatabaseSequenceFilter(connection, tables);
268             IDataSet dataset = new FilteredDataSet(filter, connection
269                     .createDataSet(tables));
270
271             DatabaseOperation.DELETE_ALL.execute(connection, dataset);
272         } catch (DatabaseUnitException e) {
273             SQLException exc = new SQLException(e.getMessage());
274             exc.initCause(e);
275             throw exc;
276         } 
277     }
278
279     /**
280      * @throws SQLException
281      */
282     public String[] getTableNames() throws SQLException {
283
284         List result = new ArrayList();
285         LOG.debug("Getting database table names to clean (schema: '"
286                 + SCHEMA_PATTERN + "'");
287
288         ResultSet tables = getConnection().getMetaData().getTables(null,
289                 SCHEMA_PATTERN, "%", new String[] { "TABLE" });
290         while (tables.next()) {
291             String table = tables.getString("TABLE_NAME");
292             // Make sure we do not touch hibernate's specific
293             // infrastructure tables.
294             if (!table.toLowerCase().startsWith("hibernate")) {
295                 result.add(table);
296                 LOG.debug("Adding " + table
297                         + " to list of tables to be cleaned.");
298             }
299         }
300         return (String[]) result.toArray(new String[0]);
301     }
302
303     /**
304      * @return
305      * @throws SQLException
306      */
307     public void emptyTables(List aTableList) throws SQLException {
308         Iterator liTable = aTableList.iterator();
309         while (liTable.hasNext()) {
310             emptyTable((String) liTable.next());
311         }
312     }
313
314     /**
315      * @return
316      * @throws SQLException
317      */
318     public void emptyTable(String aTable) throws SQLException {
319         executeSql("delete from " + aTable);
320     }
321
322     /**
323      * @return
324      * @throws SQLException
325      */
326     public void dropTable(String aTable) throws SQLException {
327         executeQuery("drop table " + aTable);
328     }
329
330     /**
331      * Executes an SQL statement within a transaction.
332      * 
333      * @param aSql
334      *            SQL statement.
335      * @return Return code of the corresponding JDBC call.
336      */
337     public int executeSql(final String aSql) {
338         return executeSql(aSql, new Object[0]);
339     }
340
341     /**
342      * Executes an SQL statement within a transaction. See
343      * {@link #setPreparedParam(int, PreparedStatement, Object)}for details on
344      * supported argument types.
345      * 
346      * @param aSql
347      *            SQL statement.
348      * @param aArg
349      *            Argument of the sql statement.
350      * @return Return code of the corresponding JDBC call.
351      */
352     public int executeSql(final String aSql, final Object aArg) {
353         return executeSql(aSql, new Object[] { aArg });
354     }
355
356     /**
357      * Executes an sql statement. See
358      * {@link #setPreparedParam(int, PreparedStatement, Object)}for details on
359      * supported argument types.
360      * 
361      * @param aSql
362      *            SQL query to execute.
363      * @param aArgs
364      *            Arguments.
365      * @return Number of rows updated.
366      */
367     public int executeSql(final String aSql, final Object[] aArgs) {
368         Map results = executeTransaction(new TestTransactionCallback() {
369             public Map execute() throws Exception {
370                 JdbcTemplate template = new JdbcTemplate(getDataSource());
371                 int result = template.update(aSql, aArgs);
372
373                 Map map = new TreeMap();
374                 map.put("result", new Integer(result));
375
376                 return map;
377             }
378         });
379
380         return ((Integer) results.get("result")).intValue();
381     }
382
383     /**
384      * Executes a transaction with a result.
385      * 
386      * @param aCallback
387      *            Callback to do your transactional work.
388      * @return Result.
389      */
390     public Object executeTransaction(TransactionCallback aCallback) {
391         TransactionTemplate lTemplate = new TransactionTemplate(
392                 getTransactionManager());
393         return lTemplate.execute(aCallback);
394     }
395
396     /**
397      * Executes a transaction without a result.
398      * 
399      * @param aCallback
400      *            Callback to do your transactional work. .
401      */
402     protected void executeTransaction(TransactionCallbackWithoutResult aCallback) {
403         TransactionTemplate template = new TransactionTemplate(
404                 getTransactionManager());
405         template.execute(aCallback);
406     }
407
408     /**
409      * Executes a transaction with a result, causing the testcase to fail if any
410      * type of exception is thrown.
411      * 
412      * @param aCallback
413      *            Code to be executed within the transaction.
414      * @return Result.
415      */
416     public Map executeTransaction(final TestTransactionCallback aCallback) {
417         return (Map) executeTransaction(new TransactionCallback() {
418             public Object doInTransaction(TransactionStatus aArg) {
419                 try {
420                     return aCallback.execute();
421                 } catch (Exception e) {
422                     // test case must fail.
423                     e.printStackTrace();
424                     throw new RuntimeException(e);
425                 }
426             }
427         });
428     }
429
430     /**
431      * Executes a transaction with a result, causing the testcase to fail if any
432      * type of exception is thrown.
433      * 
434      * @param aCallback
435      *            Code to be executed within the transaction.
436      */
437     public void executeTransaction(
438             final TestTransactionCallbackWithoutResult aCallback) {
439         executeTransaction(new TransactionCallbackWithoutResult() {
440             public void doInTransactionWithoutResult(TransactionStatus aArg) {
441                 try {
442                     aCallback.execute();
443                 } catch (Exception e) {
444                     // test case must fail.
445                     throw new RuntimeException(e.getMessage(), e);
446                 }
447             }
448         });
449     }
450
451     /**
452      * Executes an SQL query within a transaction.
453      * 
454      * @param aSql
455      *            Query to execute.
456      * @return Result set.
457      */
458     public ResultSet executeQuery(String aSql) {
459         return executeQuery(aSql, new Object[0]);
460     }
461
462     /**
463      * Executes a query with a single argument. See
464      * {@link #setPreparedParam(int, PreparedStatement, Object)}for details on
465      * supported argument types.
466      * 
467      * @param aSql
468      *            Query.
469      * @param aArg
470      *            Argument.
471      * @return Result set.
472      */
473     public ResultSet executeQuery(String aSql, Object aArg) {
474         return executeQuery(aSql, new Object[] { aArg });
475     }
476
477     /**
478      * Executes a query within a transaction. See
479      * {@link #setPreparedParam(int, PreparedStatement, Object)}for details on
480      * supported argument types.
481      * 
482      * @param aSql
483      *            Sql query.
484      * @param aArgs
485      *            Arguments to the query.
486      * @return Result set.
487      */
488     public ResultSet executeQuery(final String aSql, final Object[] aArgs) {
489         Map results = executeTransaction(new TestTransactionCallback() {
490             public Map execute() throws Exception {
491                 Connection connection = getConnection();
492
493                 PreparedStatement statement = connection.prepareStatement(aSql);
494                 setPreparedParams(aArgs, statement);
495
496                 ResultSet resultSet = statement.executeQuery();
497                 TreeMap results = new TreeMap();
498                 results.put("resultSet", resultSet);
499
500                 return results;
501             }
502         });
503
504         return (ResultSet) results.get("resultSet");
505     }
506
507     /**
508      * Sets the values of a prepared statement. See
509      * {@link #setPreparedParam(int, PreparedStatement, Object)}for details on
510      * supported argument types.
511      * 
512      * @param aArgs
513      *            Arguments to the prepared statement.
514      * @param aStatement
515      *            Prepared statement
516      * @throws SQLException
517      */
518     private void setPreparedParams(final Object[] aArgs,
519             PreparedStatement aStatement) throws SQLException {
520         for (int i = 1; i <= aArgs.length; i++) {
521             setPreparedParam(i, aStatement, aArgs[i - 1]);
522         }
523     }
524
525     /**
526      * Sets a prepared statement parameter.
527      * 
528      * @param aIndex
529      *            Index of the parameter.
530      * @param aStatement
531      *            Prepared statement.
532      * @param aObject
533      *            Value Must be of type Integer, Long, or String. TODO extend
534      *            with more types of values.
535      * @throws SQLException
536      */
537     private void setPreparedParam(int aIndex, PreparedStatement aStatement,
538             Object aObject) throws SQLException {
539         if (aObject instanceof Integer) {
540             aStatement.setInt(aIndex, ((Integer) aObject).intValue());
541         } else if (aObject instanceof Long) {
542             aStatement.setLong(aIndex, ((Integer) aObject).longValue());
543         } else if (aObject instanceof String) {
544             aStatement.setString(aIndex, (String) aObject);
545         } else {
546             TestCase.fail("Unsupported object type for prepared statement: "
547                     + aObject.getClass() + " value: " + aObject
548                     + " statement: " + aStatement);
549         }
550     }
551     
552     private boolean isDatabaseConfigured() { 
553         try { 
554             getDataSource(); 
555         } catch (NoSuchBeanDefinitionException e ) { 
556             return false; 
557         }
558         return true; 
559     }
560
561     /**
562      * @return Returns the dataSource.
563      */
564     public DataSource getDataSource() {
565         DataSource ds = (DriverManagerDataSource) getSpringContext().getBean(DATA_SOURCE);
566         assertNotNull(ds); 
567         return ds; 
568     }
569
570     /**
571      * @return
572      * @throws SQLException
573      */
574     protected int getTableSize(String aTable) throws SQLException {
575         ResultSet resultSet = executeQuery("select * from " + aTable);
576         int count = 0;
577
578         while (resultSet.next()) {
579             count++;
580         }
581
582         return count;
583     }
584     
585     protected int countResultSet(ResultSet aResultSet) throws SQLException { 
586         int count = 0;
587
588         while (aResultSet.next()) {
589             count++;
590         }
591
592         return count;
593     }
594
595 }