001: // $Id: DDTTestCase.java 282 2007-07-19 22:46:27Z jg_hamburg $
002: /********************************************************************************
003: * DDTUnit, a Datadriven Approach to Unit- and Moduletesting
004: * Copyright (c) 2004, Joerg and Kai Gellien
005: * All rights reserved.
006: *
007: * The Software is provided under the terms of the Common Public License 1.0
008: * as provided with the distribution of DDTUnit in the file cpl-v10.html.
009: * Redistribution and use in source and binary forms, with or without
010: * modification, are permitted provided that the following conditions
011: * are met:
012: *
013: * + Redistributions of source code must retain the above copyright
014: * notice, this list of conditions and the following disclaimer.
015: *
016: * + Redistributions in binary form must reproduce the above
017: * copyright notice, this list of conditions and the following
018: * disclaimer in the documentation and/or other materials provided
019: * with the distribution.
020: *
021: * + Neither the name of the authors or DDTUnit, nor the
022: * names of its contributors may be used to endorse or promote
023: * products derived from this software without specific prior
024: * written permission.
025: *
026: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
027: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
028: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
029: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
030: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
031: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
032: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
033: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
034: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
035: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
036: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
037: ********************************************************************************/package junitx.ddtunit;
038:
039: import java.lang.reflect.Method;
040: import java.lang.reflect.Modifier;
041: import java.util.Iterator;
042: import java.util.List;
043: import java.util.Map.Entry;
044:
045: import junit.framework.AssertionFailedError;
046: import junit.framework.TestCase;
047: import junit.framework.TestResult;
048: import junitx.ddtunit.data.AssertObject;
049: import junitx.ddtunit.data.DDTDataRepository;
050: import junitx.ddtunit.data.DDTTestDataException;
051: import junitx.ddtunit.data.ObjectAsserter;
052: import junitx.ddtunit.data.ResourceNameFactory;
053: import junitx.ddtunit.data.TestClusterDataSet;
054: import junitx.ddtunit.data.TypedObject;
055: import junitx.ddtunit.data.TypedObjectMap;
056: import junitx.ddtunit.util.ClassAnalyser;
057: import junitx.ddtunit.util.DDTConfiguration;
058:
059: import org.apache.log4j.Logger;
060:
061: /**
062: * This class is derived from {@link TestCase}from JUnit. <br/>It will
063: * implement all neccessary features to run tests based on xml parameter data.
064: *
065: * @author jg
066: */
067: abstract public class DDTTestCase extends TestCase {
068: private static final String LF = System
069: .getProperty("line.separator");
070:
071: private Logger log = Logger.getLogger(DDTTestCase.class);
072:
073: TestClusterDataSet classDataSet;
074:
075: private String testName;
076:
077: private DDTTestResult testResult;
078:
079: private ExceptionHandler exHandler;
080:
081: private TypedObjectMap assertMap;
082:
083: private StringBuffer assertMessages;
084:
085: {
086: InternalLogger.getInstance();
087: }
088:
089: /**
090: *
091: */
092: public DDTTestCase() {
093: super ();
094: this .assertMessages = new StringBuffer();
095: }
096:
097: /**
098: * @param name
099: * of testmethod to execute
100: */
101: public DDTTestCase(String name) {
102: super (name);
103: this .assertMessages = new StringBuffer();
104: }
105:
106: /**
107: * Run complete selected testmethod by generating a separate result und
108: * return this after execution. This contains all notification hooks to
109: * TestListener classes like TestRunner.
110: *
111: * @return result of executed test
112: */
113: public TestResult run() {
114: try {
115: log.debug("run() - START");
116:
117: DDTTestResult result = new DDTTestResult();
118:
119: this .testResult = result;
120: run(result);
121:
122: return result;
123: } finally {
124: log.debug("run() - END");
125: }
126: }
127:
128: /**
129: * Run complete selected testmethod by generating a separate result und
130: * return this after execution. This contains all notification hooks to
131: * TestListener classes like TestRunner.
132: *
133: * @param result
134: * object generated externally, by a testrunner e.g..
135: */
136: public void run(TestResult result) {
137: log.debug("run(TestResult) - START");
138:
139: DDTTestResult ddtResult;
140:
141: if (DDTTestResult.class.isInstance(result)) {
142: ddtResult = (DDTTestResult) result;
143: } else {
144: ddtResult = new DDTTestResult(result);
145: }
146:
147: this .testResult = ddtResult;
148: ddtResult.run(this );
149:
150: if (!DDTTestResult.class.isInstance(result)) {
151: ddtResult.copyContent(result);
152: }
153:
154: log.debug("run(TestResult) - END");
155: }
156:
157: /**
158: * Run a bare method cycle as defined in JUnit. Here the testdata
159: * initialization is performed. Because every testmethod should be run under
160: * its own fixture the execution of setUp and tearDown is inside of a
161: * subroutine. These methods will be executed inside of around every test
162: * representation of xml testdata definition.
163: *
164: * @throws Throwable
165: * that might come up during testmethod execution.
166: */
167: public void runBare() throws Throwable {
168: log.debug("runBare() - START");
169: DDTConfiguration.getInstance().load();
170: // initialize xml testdata
171: initContext();
172:
173: try {
174: runMethodTest();
175: } finally {
176: }
177:
178: log.debug("runBare() - END");
179: }
180:
181: /**
182: * Retrieve object with specified identifier on a per method-test basis.
183: * <br/>If no data exists an exception will be raised.
184: *
185: * @param objectId
186: * specifies key for retrieval
187: *
188: * @return Object that is stored under identifier key
189: */
190: protected Object getObject(String objectId) {
191: Object obj = null;
192: TypedObject typedObject;
193:
194: if (!this .classDataSet.containsKey(this .getName())) {
195: throw new DDTException("No objects defined <" + objectId
196: + "> in method scope");
197: }
198:
199: typedObject = this .classDataSet.getObject(this .getName(), this
200: .getTestName(), objectId);
201:
202: if (typedObject == null) {
203: throw new DDTTestDataException(
204: "Error retrieving testdata, could not find object("
205: + objectId + ")");
206: } else {
207: obj = typedObject.getValue();
208: }
209: return obj;
210: }
211:
212: /**
213: * Retrieve object with specified identifier on a per method-test basis.
214: * <br/>If no data exists an exception will be raised.
215: *
216: * @param objectId
217: * specifies key for retrieval
218: *
219: * @return Object that is stored under identifier key
220: */
221: protected Object getObject(String objectId, String objectType) {
222: Object obj = null;
223: TypedObject typedObject;
224:
225: if (!this .classDataSet.containsKey(this .getName())) {
226: throw new DDTException("No objects defined <" + objectId
227: + "> in method scope");
228: }
229:
230: typedObject = this .classDataSet.getObject(this .getName(), this
231: .getTestName(), objectId, objectType);
232:
233: if (typedObject == null) {
234: throw new DDTTestDataException(
235: "Error retrieving testdata, could not find object("
236: + objectId + " of type " + objectType + ")");
237: } else {
238: obj = typedObject.getValue();
239: }
240: return obj;
241: }
242:
243: /**
244: * Retrieve object with specified identifier on a per class basis. <br/>If
245: * no data exists an exception will be raised.
246: *
247: * @param objectId
248: * specifies key for retrieval
249: *
250: * @return Object that is stored under identifier key
251: */
252: protected Object getGlobalObject(String objectId) {
253: Object obj = null;
254: TypedObject typedObject = this .classDataSet
255: .findObject(objectId);
256:
257: if (typedObject == null) {
258: throw new DDTTestDataException(
259: "Error retrieving testdata, could not find object.");
260: } else {
261: obj = typedObject.getValue();
262: }
263: return obj;
264: }
265:
266: /**
267: * Retrieve object with specified identifier on a per class basis. <br/>If
268: * no data exists an exception will be raised.
269: *
270: * @param objectId
271: * specifies key for retrieval
272: *
273: * @return Object that is stored under identifier key
274: */
275: protected Object getGlobalObject(String objectId, String objectType) {
276: Object obj = null;
277: TypedObject typedObject = this .classDataSet.findObject(
278: objectId, objectType);
279:
280: if (typedObject != null) {
281: obj = typedObject.getValue();
282: } else {
283: throw new DDTTestDataException(
284: "Error retrieving testdata, could not find object.");
285: }
286: return obj;
287: }
288:
289: /**
290: * Retrieve object with specified identifier on class independend basis.
291: * <br/>If no data exists an exception will be raised.
292: *
293: * @param objectId
294: * specifies key for retrieval
295: *
296: * @return Object that is stored under identifier key
297: */
298: protected Object getResourceObject(String objectId) {
299: Object obj = null;
300: TypedObject typedObject = DDTDataRepository.getInstance()
301: .getObject(objectId);
302: if (typedObject == null) {
303: throw new DDTTestDataException(
304: "Error retrieving testdata, could not find object.");
305: }
306: obj = typedObject.getValue();
307: return obj;
308: }
309:
310: /**
311: * Retrieve object with specified identifier on class independend basis.
312: * <br/>If no data exists an exception will be raised.
313: *
314: * @param objectId
315: * specifies key for retrieval
316: *
317: * @return Object that is stored under identifier key
318: */
319: protected Object getResourceObject(String objectId,
320: String objectType) {
321: Object obj = null;
322: TypedObject typedObject = DDTDataRepository.getInstance()
323: .getObject(objectId, objectType);
324: obj = typedObject.getValue();
325: return obj;
326: }
327:
328: /**
329: * Add object to make assertion against assert definition identified by
330: * assertId
331: *
332: * @param assertId
333: * to identify assert
334: * @param object
335: * used as actual object against expected object defined in
336: * assertion
337: */
338: protected void addObjectToAssert(String assertId, Object object) {
339: addAssertInfo(assertId, object);
340: }
341:
342: /**
343: * Add actual object information to internal assert record.
344: *
345: * @param assertId
346: * to identify assert record
347: * @param obj
348: * value of actual value to assert
349: */
350: private AssertObject addAssertInfo(String assertId, Object object) {
351: AssertObject ar;
352:
353: if (!this .classDataSet.containsKey(this .getName())) {
354: throw new DDTException("No asserts defined in method scope");
355: }
356:
357: ar = (AssertObject) this .assertMap.get(assertId);
358:
359: if (ar == null) {
360: throw new DDTException("Assert \"" + assertId
361: + "\" does not exist in resource.");
362: }
363:
364: ar.setActualObject(object);
365: return ar;
366: }
367:
368: /**
369: * Directly assert expected against actual object during method execution.
370: * Do not wait till the end of test execution.
371: *
372: * @param assertId
373: * to retrieve expected assert
374: * @param obj
375: * to assert against expected value
376: */
377: protected void assertObject(String assertId, Object obj) {
378: assertObject(assertId, obj, true);
379: }
380:
381: /**
382: * Directly assert expected against actual object during method execution.
383: * Do not wait till the end of test execution.
384: *
385: * @param assertId
386: * to retrieve expected assert
387: * @param obj
388: * to assert against expected value
389: * @param mark
390: * true if validation should be marked as executed
391: */
392: protected void assertObject(String assertId, Object obj,
393: boolean mark) {
394: AssertObject ar = addAssertInfo(assertId, obj);
395: ar.validate(mark);
396: }
397:
398: /**
399: * Validate all assertions concerning the active method-test dataset. All
400: * asserts that are not marked as allready processed are validated and
401: * marked as processed.
402: */
403: protected void validateAsserts(boolean assertSupport) {
404: if (assertSupport
405: && this .classDataSet.containsTest(this .getName(), this
406: .getTestName())) {
407: for (Iterator iter = assertMap.entrySet().iterator(); iter
408: .hasNext();) {
409: Entry assertEntry = (Entry) iter.next();
410: TypedObject assertObj = (TypedObject) assertEntry
411: .getValue();
412: if (ObjectAsserter.class.isInstance(assertObj)) {
413: ObjectAsserter oa = (ObjectAsserter) assertObj;
414: if (!oa.isValidated()) {
415: try {
416: oa.validate(true);
417: } catch (AssertionFailedError ex) {
418: if (DDTConfiguration.getInstance()
419: .isSpecificationAssert()) {
420: if (!"".equals(this .assertMessages)) {
421: this .assertMessages.append(LF);
422: }
423: this .assertMessages.append(oa).append(
424: ex.getMessage());
425: } else {
426: throw ex;
427: }
428: }
429: }
430: }
431: }
432: }
433: }
434:
435: /**
436: * Implement method for initializing test context. Especially retrieving
437: * test data resource. <br/>The easies way to do this is just to use the
438: * following code snipplet: <code><pre>
439: * void initContext() {
440: * initTestData("/mySpecialResource.xml", "ClassIdInResourceToUse");
441: * }
442: * </pre></code>
443: */
444: abstract protected void initContext();
445:
446: /**
447: * Initialize xml test data for specified classId in resource associagted to
448: * same name as classId using {@link ResourceNameFactory}.
449: *
450: * @param resource
451: * of xml based test data
452: */
453: protected void initTestData(String classId) {
454: initTestData(classId, classId);
455: }
456:
457: /**
458: * Initialize xml test data for specified classId in resource
459: *
460: * @param resource
461: * of xml based test data
462: * @param classId
463: * of test data to process
464: */
465: protected void initTestData(String resource, String classId) {
466: String resourceName = ResourceNameFactory.getInstance()
467: .getName(ClassAnalyser.classPackage(this ), resource);
468: log.debug("parse() - resource to process: " + resourceName);
469: this .classDataSet = DDTDataRepository.getInstance().get(
470: resourceName, classId);
471: }
472:
473: /**
474: * Do not use this method to define tests. Use <code>test<name>()
475: * </code>
476: * instead.
477: */
478: protected void runTest() {
479: throw new DDTException(
480: "It is forbidden to use DDTTestCase.runTest()");
481: }
482:
483: /**
484: * Execute the testmethod without extra setUp and tearDown methods and no
485: * hooks to TestListener classes like TestRunner. <br/>This method contains
486: * the iteration over the xml defined tests per method.
487: *
488: * @throws Throwable
489: * on any exception that occures
490: */
491: protected void runMethodTest() throws Throwable {
492: log.debug("runMethodTest() - START");
493:
494: String fName = getName();
495:
496: assertNotNull(fName);
497:
498: Method runMethod = null;
499: try {
500: // use getMethod to get all public inherited
501: // methods. getDeclaredMethods returns all
502: // methods of this class but excludes the
503: // inherited ones.
504: runMethod = getClass().getMethod(fName, null);
505: } catch (NoSuchMethodException e) {
506: fail("Method \"" + fName + "\" not found");
507: }
508:
509: if (!Modifier.isPublic(runMethod.getModifiers())) {
510: fail("Method \"" + fName + "\" should be public");
511: }
512:
513: // process iteration over tests per method. if no data available (JUnit
514: // TestCase)
515: // just do simple method invokation
516: this .exHandler = new ExceptionHandler(this .getName());
517: if (this .classDataSet.get(this .getName()) == null
518: || this .classDataSet.size(this .getName()) == 0) {
519: testResult.startMethodTest(this , "no-testdata");
520: processMethodTest(runMethod, this .getName(), null);
521: } else {
522: List<String> orderedTestKeys = this .classDataSet
523: .getOrderedTestKeys(this .getName());
524:
525: for (String testId : orderedTestKeys) {
526: testResult.startMethodTest(this , testId);
527: this .testName = testId;
528: this .assertMap = (TypedObjectMap) this .classDataSet
529: .getAssertMap(this .getName(), testId).clone();
530: // reset assert error buffer of one testcase execution
531: this .assertMessages.delete(0, this .assertMessages
532: .length());
533: processMethodTest(runMethod, testId, this .assertMap);
534: }
535: }
536: int totalCount = this .classDataSet.size(this .getName());
537: this .exHandler.summarizeProblems(totalCount == 0 ? 1
538: : totalCount);
539:
540: log.debug("runMethodTest() - END");
541: }
542:
543: /**
544: * Process one xml based test of runMethod by executing setUp() and
545: * tearDown() around method execution.
546: *
547: * @param runMethod
548: * to process from testclass
549: * @param testId
550: * of test to run
551: * @param assertMap
552: * containing all info about expected exception
553: * @throws Throwable
554: */
555: private void processMethodTest(Method runMethod, String testId,
556: TypedObjectMap assertMap) throws Throwable {
557: boolean executeMethod = false;
558: try {
559: this .setUp();
560: executeMethod = true;
561: } catch (Throwable ex) {
562: DDTException ddtEx;
563: if (DDTException.class.isInstance(ex)) {
564: ddtEx = (DDTException) ex;
565: } else {
566: ddtEx = new DDTSetUpException(ex.getMessage(), ex);
567: }
568: this .testResult.addMethodTestError(this , testId, ddtEx);
569: }
570: // only if no error is raised the method trunk should be executed
571: if (executeMethod) {
572: try {
573: runMethod.setAccessible(true);
574: runMethod.invoke(this , new Object[0]);
575: validateAsserts(DDTConfiguration.getInstance()
576: .isActiveAsserts());
577: // a set of assert errors where catched during processing
578: if (!"".equals(this .assertMessages.toString())) {
579: throw new AssertionFailedError(this .assertMessages
580: .toString());
581: }
582: this .exHandler.checkOnExpectedException(testId,
583: assertMap);
584: } catch (Throwable ex1) {
585: try {
586: this .exHandler.process(testId, ex1, assertMap);
587: } catch (AssertionFailedError ex) {
588: this .testResult.addMethodTestFailure(this , testId,
589: ex);
590: } catch (Throwable ex) {
591: this .testResult
592: .addMethodTestError(this , testId, ex);
593: }
594: } finally {
595: this .assertMessages = new StringBuffer();
596: try {
597: this .tearDown();
598: } catch (Throwable ex) {
599: DDTException ddtEx;
600: if (DDTException.class.isInstance(ex)) {
601: ddtEx = (DDTException) ex;
602: } else {
603: ddtEx = new DDTTearDownException(ex
604: .getMessage(), ex);
605: }
606: this .testResult.addMethodTestError(this , testId,
607: ddtEx);
608: }
609: testResult.endMethodTest(this , testId);
610: log.debug("runTest() - processed method \"" + getName()
611: + "\" testId \"" + testId + "\"");
612: }
613: }
614: }
615:
616: /**
617: * Count number of test datasets provided for method methodName. <br/>If
618: * dataset for this method is null, 1 will be returned (a standard JUnit
619: * method)
620: *
621: * @return Count of tests under method methodName
622: */
623: public int countMethodTests() {
624: int testCount = 1;
625:
626: if (this .classDataSet != null
627: && this .classDataSet.size(this .getName()) > 0) {
628: testCount = this .classDataSet.size(this .getName());
629: } else if (this .classDataSet == null) {
630: testCount = -1;
631: }
632:
633: return testCount;
634: }
635:
636: /**
637: * @return Information about actual run test
638: */
639: public String runInfo() {
640: StringBuffer sb = new StringBuffer();
641:
642: sb.append("Test class: ").append(this .getClass().getName())
643: .append(", method: ").append(this .getName()).append(LF);
644:
645: return sb.toString();
646: }
647:
648: public String getTestName() {
649: return testName;
650: }
651:
652: public void setTestName(String testName) {
653: this.testName = testName;
654: }
655:
656: }
|