View Javadoc

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