view bindings/as3/ext/asunit/framework/TestCase.as @ 770:c54bc2ffbf92 tip

update tags
author convert-repo
date Fri, 16 Dec 2011 11:34:01 +0000
parents 3a0b9700b3d2
children
line wrap: on
line source
package asunit.framework {
	import flash.display.DisplayObject;
	import flash.display.DisplayObjectContainer;
	import flash.errors.IllegalOperationError;
	import flash.events.Event;
	import flash.utils.describeType;
	import flash.utils.getDefinitionByName;
	import flash.utils.setTimeout;

	import asunit.errors.AssertionFailedError;
	import asunit.util.ArrayIterator;
	import asunit.util.Iterator;

	/**
	 * A test case defines the fixture to run multiple tests. To define a test case<br>
	 * 1) implement a subclass of TestCase<br>
	 * 2) define instance variables that store the state of the fixture<br>
	 * 3) initialize the fixture state by overriding <code>setUp</code><br>
	 * 4) clean-up after a test by overriding <code>tearDown</code>.<br>
	 * Each test runs in its own fixture so there
	 * can be no side effects among test runs.
	 * Here is an example:
	 * <listing>
	 * public class MathTest extends TestCase {
	 *      private var value1:Number;
	 *      private var value2:Number;
	 *
	 *      public function MathTest(methodName:String=null) {
	 *         super(methodName);
	 *      }
	 *
	 *      override protected function setUp():void {
	 *         super.setUp();
	 *         value1 = 2;
	 *         value2 = 3;
	 *      }
	 * }
	 * </listing>
	 *
	 * For each test implement a method which interacts
	 * with the fixture. Verify the expected results with assertions specified
	 * by calling <code>assertTrue</code> with a boolean, or <code>assertEquals</code>
	 * with two primitive values that should match.
	 * <listing>
	 *    public function testAdd():void {
	 *        var result:Number = value1 + value2;
	 *        assertEquals(5, result);
	 *    }
	 * </listing>
	 *
	 *  There are three common types of test cases:
	 *
	 *  <ol>
	 *  <li>Simple unit test</li>
	 *  <li>Visual integration test</li>
	 *  <li>Asynchronous test</li>
	 *  </ol>
	 *
	 *  @includeExample MathUtilTest.as
	 *  @includeExample ComponentTestIntroduction.as
	 *  @includeExample ComponentUnderTest.as
	 *  @includeExample ComponentTestExample.as
	 *  @includeExample AsynchronousTestMethodExample.as
	 */
	public class TestCase extends Assert implements Test {
		protected static const PRE_SET_UP:int        = 0;
		protected static const SET_UP:int             = 1;
		protected static const RUN_METHOD:int         = 2;
		protected static const TEAR_DOWN:int        = 3;
		protected static const DEFAULT_TIMEOUT:int     = 1000;

		protected var context:DisplayObjectContainer;
		protected var fName:String;
		protected var isComplete:Boolean;
		protected var result:TestListener;
		protected var testMethods:Array;

		private var asyncQueue:Array;
		private var currentMethod:String;
		private var currentState:int;
		private var layoutManager:Object;
		private var methodIterator:Iterator;
		private var runSingle:Boolean;

		/**
		 * Constructs a test case with the given name.
		 *
		 * Be sure to implement the constructor in your own TestCase base classes.
		 *
		 * Using the optional <code>testMethod</code> constructor parameter is how we
		 * create and run a single test case and test method.
		 */
		public function TestCase(testMethod:String = null) {
			var description:XML = describeType(this);
			var className:Object = description.@name;
			var methods:XMLList = description..method.((@name+"").match("^test"));
			if(testMethod != null) {
				testMethods = testMethod.split(", ").join(",").split(",");
				if(testMethods.length == 1) {
					runSingle = true;
				}
			} else {
				setTestMethods(methods);
			}
			setName(className.toString());
			resolveLayoutManager();
			asyncQueue = [];
		}

		private function resolveLayoutManager():void {
			// Avoid creating import dependencies on flex framework
			// If you have the framework.swc in your classpath,
			// the layout manager will be found, if not, a mcok
			// will be used.
			try {
				var manager:Class = getDefinitionByName("mx.managers.LayoutManager") as Class;
				layoutManager = manager["getInstance"]();
				if(!layoutManager.hasOwnProperty("resetAll")) {
					throw new Error("TestCase :: mx.managers.LayoutManager missing resetAll method");
				}
			}
			catch(e:Error) {
				layoutManager = new Object();
				layoutManager.resetAll = function():void {
				};
			}
		}

		/**
		 * Sets the name of a TestCase
		 * @param name The name to set
		 */
		public function setName(name:String):void {
			fName = name;
		}

		protected function setTestMethods(methodNodes:XMLList):void {
			testMethods = new Array();
			var methodNames:Object = methodNodes.@name;
			var name:String;
			for each(var item:Object in methodNames) {
				name = item.toString();
				testMethods.push(name);
			}
		}

		public function getTestMethods():Array {
			return testMethods;
		}

		/**
		 * Counts the number of test cases executed by run(TestResult result).
		 */
		public function countTestCases():int {
			return testMethods.length;
		}

		/**
		 * Creates a default TestResult object
		 *
		 * @see TestResult
		 */
		protected function createResult():TestResult {
			return new TestResult();
		}

		/**
		 * A convenience method to run this test, collecting the results with
		 * either the TestResult provided or a default, new TestResult object.
		 * Expects either:
		 * run():void // will return the newly created TestResult
		 * run(result:TestResult):TestResult // will use the TestResult
		 * that was passed in.
		 *
		 * @see TestResult
		 */
		public function run():void {
			getResult().run(this);
		}

		public function setResult(result:TestListener):void {
			this.result = result;
		}

		internal function getResult():TestListener {
			return (result == null) ? createResult() : result;
		}

		/**
		 * Runs the bare test sequence.
		 * @throws Error if any exception is thrown
		 */
		public function runBare():void {
			if(isComplete) {
				return;
			}
			var name:String;
			var itr:Iterator = getMethodIterator();
			if(itr.hasNext()) {
				name = String(itr.next());
				currentState = PRE_SET_UP;
				runMethod(name);
			}
			else {
				cleanUp();
				getResult().endTest(this);
				isComplete = true;
				dispatchEvent(new Event(Event.COMPLETE));
			}
		}

		private function getMethodIterator():Iterator {
			if(methodIterator == null) {
				methodIterator = new ArrayIterator(testMethods);
			}
			return methodIterator;
		}

		/**
		*   Override this method in Asynchronous test cases
		*   or any other time you want to perform additional
		*   member cleanup after all test methods have run
		**/
		protected function cleanUp():void {
		}

		private function runMethod(methodName:String):void {
			try {
				if(currentState == PRE_SET_UP) {
					currentState = SET_UP;
					getResult().startTestMethod(this, methodName);
					setUp(); // setUp may be async and change the state of methodIsAsynchronous
				}
				currentMethod = methodName;
				if(!waitForAsync()) {
					currentState = RUN_METHOD;
					this[methodName]();
				}
			}
			catch(assertionFailedError:AssertionFailedError) {
				getResult().addFailure(this, assertionFailedError);
			}
			catch(unknownError:Error) {
				getResult().addError(this, unknownError);
			}
			finally {
				if(!waitForAsync()) {
					runTearDown();
				}
			}
		}

		/**
		 * Sets up the fixture, for example, instantiate a mock object.
		 * This method is called before each test is executed.
		 * throws Exception on error.
		 *
		 * @example This method is usually overridden in your concrete test cases:
		 *  <listing>
		 *  private var instance:MyInstance;
		 *
		 *  override protected function setUp():void {
		 *      super.setUp();
		 *      instance = new MyInstance();
		 *      addChild(instance);
		 *  }
		 *  </listing>
		 */
		protected function setUp():void {
		}
		/**
		 *  Tears down the fixture, for example, delete mock object.
		 *
		 *  This method is called after a test is executed - even if the test method
		 *  throws an exception or fails.
		 *
		 *  Even though the base class <code>TestCase</code> doesn't do anything on <code>tearDown</code>,
		 *  It's a good idea to call <code>super.tearDown()</code> in your subclasses. Many projects
		 *  wind up using some common fixtures which can often be extracted out a common project
		 *  <code>TestCase</code>.
		 *
		 *  <code>tearDown</code> is <em>not</em> called when we tell a test case to execute
		 *  a single test method.
		 *
		 *  @throws Error on error.
		 *
		 *  @example This method is usually overridden in your concrete test cases:
		 *  <listing>
		 *  private var instance:MyInstance;
		 *
		 *  override protected function setUp():void {
		 *      super.setUp();
		 *      instance = new MyInstance();
		 *      addChild(instance);
		 *  }
		 *
		 *  override protected function tearDown():void {
		 *      super.tearDown();
		 *      removeChild(instance);
		 *  }
		 *  </listing>
		 *
		 */
		protected function tearDown():void {
		}

		/**
		 * Returns a string representation of the test case
		 */
		override public function toString():String {
			if(getCurrentMethod()) {
				return getName() + "." + getCurrentMethod() + "()";
			}
			else {
				return getName();
			}
		}
		/**
		 * Gets the name of a TestCase
		 * @return returns a String
		 */
		public function getName():String {
			return fName;
		}

		public function getCurrentMethod():String {
			return currentMethod;
		}

		public function getIsComplete():Boolean {
			return isComplete;
		}

		public function setContext(context:DisplayObjectContainer):void {
			this.context = context;
		}

		/**
		*   Returns the visual <code>DisplayObjectContainer</code> that will be used by
		*   <code>addChild</code> and <code>removeChild</code> helper methods.
		**/
		public function getContext():DisplayObjectContainer {
			return context;
		}

		/**
		*   Called from within <code>setUp</code> or the body of any test method.
		*
		*   Any call to <code>addAsync</code>, will prevent test execution from continuing
		*   until the <code>duration</code> (in milliseconds) is exceeded, or the function returned by <code>addAsync</code>
		*   is called. <code>addAsync</code> can be called any number of times within a particular
		*   test method, and will block execution until each handler has returned.
		*
		*   Following is an example of how to use the <code>addAsync</code> feature:
		*   <listing>
		*   public function testDispatcher():void {
		*       var dispatcher:IEventDispatcher = new EventDispatcher();
		*       // Subscribe to an event by sending the return value of addAsync:
		*       dispatcher.addEventListener(Event.COMPLETE, addAsync(function(event:Event):void {
		*           // Make assertions *inside* your async handler:
		*           assertEquals(34, dispatcher.value);
		*       }));
		*   }
		*   </listing>
		*
		*   If you just want to verify that a particular event is triggered, you don't
		*   need to provide a handler of your own, you can do the following:
		*   <listing>
		*   public function testDispatcher():void {
		*       var dispatcher:IEventDispatcher = new EventDispatcher();
		*       dispatcher.addEventListener(Event.COMPLETE, addAsync());
		*   }
		*   </listing>
		*
		*   If you have a series of events that need to happen, you can generally add
		*   the async handler to the last one.
		*
		*   The main thing to remember is that any assertions that happen outside of the
		*   initial thread of execution, must be inside of an <code>addAsync</code> block.
		**/
		protected function addAsync(handler:Function = null, duration:Number=DEFAULT_TIMEOUT, failureHandler:Function=null):Function {
			if(handler == null) {
				handler = function(args:*):* {return;};
			}
			var async:AsyncOperation = new AsyncOperation(this, handler, duration, failureHandler);
			asyncQueue.push(async);
			return async.getCallback();
		}

		internal function asyncOperationTimeout(async:AsyncOperation, duration:Number, isError:Boolean=true):void {
			if(isError) getResult().addError(this, new IllegalOperationError("TestCase.timeout (" + duration + "ms) exceeded on an asynchronous operation."));
			asyncOperationComplete(async);
		}

		internal function asyncOperationComplete(async:AsyncOperation):void{
			// remove operation from queue
			var i:int = asyncQueue.indexOf(async);
			asyncQueue.splice(i,1);
			// if we still need to wait, return
			if(waitForAsync()) return;
			if(currentState == SET_UP) {
				runMethod(currentMethod);
			}
			else if(currentState == RUN_METHOD) {
				runTearDown();
			}
		}

		private function waitForAsync():Boolean{
			return asyncQueue.length > 0;
		}

		protected function runTearDown():void {
			if(currentState == TEAR_DOWN) {
				return;
			}
			currentState = TEAR_DOWN;
			if(isComplete) {
				return;
			}
			if(!runSingle) {
				getResult().endTestMethod(this, currentMethod);
				tearDown();
				layoutManager.resetAll();
			}
			setTimeout(runBare, 5);
		}

		/**
		* Helper method for testing <code>DisplayObject</code>s.
		*
		* This method allows you to more easily add and manage <code>DisplayObject</code>
		* instances in your <code>TestCase</code>.
		*
		* If you are using the regular <code>TestRunner</code>, you cannot add Flex classes.
		*
		* If you are using a <code>FlexRunner</code> base class, you can add either
		* regular <code>DisplayObject</code>s or <code>IUIComponent</code>s.
		*
		* Usually, this method is called within <code>setUp</code>, and <code>removeChild</code>
		* is called from within <code>tearDown</code>. Using these methods, ensures that added
		* children will be subsequently removed, even when tests fail.
		*
		* Here is an example of the <code>addChild</code> method:
		* <listing>
		*   private var instance:MyComponent;
		*
		*   override protected function setUp():void {
		*       super.setUp();
		*       instance = new MyComponent();
		*       instance.addEventListener(Event.COMPLETE, addAsync());
		*       addChild(instance);
		*   }
		*
		*   override protected function tearDown():void {
		*       super.tearDown();
		*       removeChild(instance);
		*   }
		*
		*   public function testParam():void {
		*       assertEquals(34, instance.value);
		*   }
		* </listing>
		**/
		protected function addChild(child:DisplayObject):DisplayObject {
			return getContext().addChild(child);
		}

		/**
		* Helper method for removing added <code>DisplayObject</code>s.
		*
		* <b>Update:</b> This method should no longer fail if the provided <code>DisplayObject</code>
		* has already been removed.
		**/
		protected function removeChild(child:DisplayObject):DisplayObject {
			if(child == null) {
				return null;
			}
			try {
				return getContext().removeChild(child);
			}
			catch(e:Error) {
			}
			return null;
		}
	}
}