mas01mj@732: package asunit.framework { mas01mj@732: import flash.display.DisplayObject; mas01mj@732: import flash.display.DisplayObjectContainer; mas01mj@732: import flash.errors.IllegalOperationError; mas01mj@732: import flash.events.Event; mas01mj@732: import flash.utils.describeType; mas01mj@732: import flash.utils.getDefinitionByName; mas01mj@732: import flash.utils.setTimeout; mas01mj@732: mas01mj@732: import asunit.errors.AssertionFailedError; mas01mj@732: import asunit.util.ArrayIterator; mas01mj@732: import asunit.util.Iterator; mas01mj@732: mas01mj@732: /** mas01mj@732: * A test case defines the fixture to run multiple tests. To define a test case
mas01mj@732: * 1) implement a subclass of TestCase
mas01mj@732: * 2) define instance variables that store the state of the fixture
mas01mj@732: * 3) initialize the fixture state by overriding setUp
mas01mj@732: * 4) clean-up after a test by overriding tearDown.
mas01mj@732: * Each test runs in its own fixture so there mas01mj@732: * can be no side effects among test runs. mas01mj@732: * Here is an example: mas01mj@732: * mas01mj@732: * public class MathTest extends TestCase { mas01mj@732: * private var value1:Number; mas01mj@732: * private var value2:Number; mas01mj@732: * mas01mj@732: * public function MathTest(methodName:String=null) { mas01mj@732: * super(methodName); mas01mj@732: * } mas01mj@732: * mas01mj@732: * override protected function setUp():void { mas01mj@732: * super.setUp(); mas01mj@732: * value1 = 2; mas01mj@732: * value2 = 3; mas01mj@732: * } mas01mj@732: * } mas01mj@732: * mas01mj@732: * mas01mj@732: * For each test implement a method which interacts mas01mj@732: * with the fixture. Verify the expected results with assertions specified mas01mj@732: * by calling assertTrue with a boolean, or assertEquals mas01mj@732: * with two primitive values that should match. mas01mj@732: * mas01mj@732: * public function testAdd():void { mas01mj@732: * var result:Number = value1 + value2; mas01mj@732: * assertEquals(5, result); mas01mj@732: * } mas01mj@732: * mas01mj@732: * mas01mj@732: * There are three common types of test cases: mas01mj@732: * mas01mj@732: *
    mas01mj@732: *
  1. Simple unit test
  2. mas01mj@732: *
  3. Visual integration test
  4. mas01mj@732: *
  5. Asynchronous test
  6. mas01mj@732: *
mas01mj@732: * mas01mj@732: * @includeExample MathUtilTest.as mas01mj@732: * @includeExample ComponentTestIntroduction.as mas01mj@732: * @includeExample ComponentUnderTest.as mas01mj@732: * @includeExample ComponentTestExample.as mas01mj@732: * @includeExample AsynchronousTestMethodExample.as mas01mj@732: */ mas01mj@732: public class TestCase extends Assert implements Test { mas01mj@732: protected static const PRE_SET_UP:int = 0; mas01mj@732: protected static const SET_UP:int = 1; mas01mj@732: protected static const RUN_METHOD:int = 2; mas01mj@732: protected static const TEAR_DOWN:int = 3; mas01mj@732: protected static const DEFAULT_TIMEOUT:int = 1000; mas01mj@732: mas01mj@732: protected var context:DisplayObjectContainer; mas01mj@732: protected var fName:String; mas01mj@732: protected var isComplete:Boolean; mas01mj@732: protected var result:TestListener; mas01mj@732: protected var testMethods:Array; mas01mj@732: mas01mj@732: private var asyncQueue:Array; mas01mj@732: private var currentMethod:String; mas01mj@732: private var currentState:int; mas01mj@732: private var layoutManager:Object; mas01mj@732: private var methodIterator:Iterator; mas01mj@732: private var runSingle:Boolean; mas01mj@732: mas01mj@732: /** mas01mj@732: * Constructs a test case with the given name. mas01mj@732: * mas01mj@732: * Be sure to implement the constructor in your own TestCase base classes. mas01mj@732: * mas01mj@732: * Using the optional testMethod constructor parameter is how we mas01mj@732: * create and run a single test case and test method. mas01mj@732: */ mas01mj@732: public function TestCase(testMethod:String = null) { mas01mj@732: var description:XML = describeType(this); mas01mj@732: var className:Object = description.@name; mas01mj@732: var methods:XMLList = description..method.((@name+"").match("^test")); mas01mj@732: if(testMethod != null) { mas01mj@732: testMethods = testMethod.split(", ").join(",").split(","); mas01mj@732: if(testMethods.length == 1) { mas01mj@732: runSingle = true; mas01mj@732: } mas01mj@732: } else { mas01mj@732: setTestMethods(methods); mas01mj@732: } mas01mj@732: setName(className.toString()); mas01mj@732: resolveLayoutManager(); mas01mj@732: asyncQueue = []; mas01mj@732: } mas01mj@732: mas01mj@732: private function resolveLayoutManager():void { mas01mj@732: // Avoid creating import dependencies on flex framework mas01mj@732: // If you have the framework.swc in your classpath, mas01mj@732: // the layout manager will be found, if not, a mcok mas01mj@732: // will be used. mas01mj@732: try { mas01mj@732: var manager:Class = getDefinitionByName("mx.managers.LayoutManager") as Class; mas01mj@732: layoutManager = manager["getInstance"](); mas01mj@732: if(!layoutManager.hasOwnProperty("resetAll")) { mas01mj@732: throw new Error("TestCase :: mx.managers.LayoutManager missing resetAll method"); mas01mj@732: } mas01mj@732: } mas01mj@732: catch(e:Error) { mas01mj@732: layoutManager = new Object(); mas01mj@732: layoutManager.resetAll = function():void { mas01mj@732: }; mas01mj@732: } mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Sets the name of a TestCase mas01mj@732: * @param name The name to set mas01mj@732: */ mas01mj@732: public function setName(name:String):void { mas01mj@732: fName = name; mas01mj@732: } mas01mj@732: mas01mj@732: protected function setTestMethods(methodNodes:XMLList):void { mas01mj@732: testMethods = new Array(); mas01mj@732: var methodNames:Object = methodNodes.@name; mas01mj@732: var name:String; mas01mj@732: for each(var item:Object in methodNames) { mas01mj@732: name = item.toString(); mas01mj@732: testMethods.push(name); mas01mj@732: } mas01mj@732: } mas01mj@732: mas01mj@732: public function getTestMethods():Array { mas01mj@732: return testMethods; mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Counts the number of test cases executed by run(TestResult result). mas01mj@732: */ mas01mj@732: public function countTestCases():int { mas01mj@732: return testMethods.length; mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Creates a default TestResult object mas01mj@732: * mas01mj@732: * @see TestResult mas01mj@732: */ mas01mj@732: protected function createResult():TestResult { mas01mj@732: return new TestResult(); mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * A convenience method to run this test, collecting the results with mas01mj@732: * either the TestResult provided or a default, new TestResult object. mas01mj@732: * Expects either: mas01mj@732: * run():void // will return the newly created TestResult mas01mj@732: * run(result:TestResult):TestResult // will use the TestResult mas01mj@732: * that was passed in. mas01mj@732: * mas01mj@732: * @see TestResult mas01mj@732: */ mas01mj@732: public function run():void { mas01mj@732: getResult().run(this); mas01mj@732: } mas01mj@732: mas01mj@732: public function setResult(result:TestListener):void { mas01mj@732: this.result = result; mas01mj@732: } mas01mj@732: mas01mj@732: internal function getResult():TestListener { mas01mj@732: return (result == null) ? createResult() : result; mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Runs the bare test sequence. mas01mj@732: * @throws Error if any exception is thrown mas01mj@732: */ mas01mj@732: public function runBare():void { mas01mj@732: if(isComplete) { mas01mj@732: return; mas01mj@732: } mas01mj@732: var name:String; mas01mj@732: var itr:Iterator = getMethodIterator(); mas01mj@732: if(itr.hasNext()) { mas01mj@732: name = String(itr.next()); mas01mj@732: currentState = PRE_SET_UP; mas01mj@732: runMethod(name); mas01mj@732: } mas01mj@732: else { mas01mj@732: cleanUp(); mas01mj@732: getResult().endTest(this); mas01mj@732: isComplete = true; mas01mj@732: dispatchEvent(new Event(Event.COMPLETE)); mas01mj@732: } mas01mj@732: } mas01mj@732: mas01mj@732: private function getMethodIterator():Iterator { mas01mj@732: if(methodIterator == null) { mas01mj@732: methodIterator = new ArrayIterator(testMethods); mas01mj@732: } mas01mj@732: return methodIterator; mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Override this method in Asynchronous test cases mas01mj@732: * or any other time you want to perform additional mas01mj@732: * member cleanup after all test methods have run mas01mj@732: **/ mas01mj@732: protected function cleanUp():void { mas01mj@732: } mas01mj@732: mas01mj@732: private function runMethod(methodName:String):void { mas01mj@732: try { mas01mj@732: if(currentState == PRE_SET_UP) { mas01mj@732: currentState = SET_UP; mas01mj@732: getResult().startTestMethod(this, methodName); mas01mj@732: setUp(); // setUp may be async and change the state of methodIsAsynchronous mas01mj@732: } mas01mj@732: currentMethod = methodName; mas01mj@732: if(!waitForAsync()) { mas01mj@732: currentState = RUN_METHOD; mas01mj@732: this[methodName](); mas01mj@732: } mas01mj@732: } mas01mj@732: catch(assertionFailedError:AssertionFailedError) { mas01mj@732: getResult().addFailure(this, assertionFailedError); mas01mj@732: } mas01mj@732: catch(unknownError:Error) { mas01mj@732: getResult().addError(this, unknownError); mas01mj@732: } mas01mj@732: finally { mas01mj@732: if(!waitForAsync()) { mas01mj@732: runTearDown(); mas01mj@732: } mas01mj@732: } mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Sets up the fixture, for example, instantiate a mock object. mas01mj@732: * This method is called before each test is executed. mas01mj@732: * throws Exception on error. mas01mj@732: * mas01mj@732: * @example This method is usually overridden in your concrete test cases: mas01mj@732: * mas01mj@732: * private var instance:MyInstance; mas01mj@732: * mas01mj@732: * override protected function setUp():void { mas01mj@732: * super.setUp(); mas01mj@732: * instance = new MyInstance(); mas01mj@732: * addChild(instance); mas01mj@732: * } mas01mj@732: * mas01mj@732: */ mas01mj@732: protected function setUp():void { mas01mj@732: } mas01mj@732: /** mas01mj@732: * Tears down the fixture, for example, delete mock object. mas01mj@732: * mas01mj@732: * This method is called after a test is executed - even if the test method mas01mj@732: * throws an exception or fails. mas01mj@732: * mas01mj@732: * Even though the base class TestCase doesn't do anything on tearDown, mas01mj@732: * It's a good idea to call super.tearDown() in your subclasses. Many projects mas01mj@732: * wind up using some common fixtures which can often be extracted out a common project mas01mj@732: * TestCase. mas01mj@732: * mas01mj@732: * tearDown is not called when we tell a test case to execute mas01mj@732: * a single test method. mas01mj@732: * mas01mj@732: * @throws Error on error. mas01mj@732: * mas01mj@732: * @example This method is usually overridden in your concrete test cases: mas01mj@732: * mas01mj@732: * private var instance:MyInstance; mas01mj@732: * mas01mj@732: * override protected function setUp():void { mas01mj@732: * super.setUp(); mas01mj@732: * instance = new MyInstance(); mas01mj@732: * addChild(instance); mas01mj@732: * } mas01mj@732: * mas01mj@732: * override protected function tearDown():void { mas01mj@732: * super.tearDown(); mas01mj@732: * removeChild(instance); mas01mj@732: * } mas01mj@732: * mas01mj@732: * mas01mj@732: */ mas01mj@732: protected function tearDown():void { mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Returns a string representation of the test case mas01mj@732: */ mas01mj@732: override public function toString():String { mas01mj@732: if(getCurrentMethod()) { mas01mj@732: return getName() + "." + getCurrentMethod() + "()"; mas01mj@732: } mas01mj@732: else { mas01mj@732: return getName(); mas01mj@732: } mas01mj@732: } mas01mj@732: /** mas01mj@732: * Gets the name of a TestCase mas01mj@732: * @return returns a String mas01mj@732: */ mas01mj@732: public function getName():String { mas01mj@732: return fName; mas01mj@732: } mas01mj@732: mas01mj@732: public function getCurrentMethod():String { mas01mj@732: return currentMethod; mas01mj@732: } mas01mj@732: mas01mj@732: public function getIsComplete():Boolean { mas01mj@732: return isComplete; mas01mj@732: } mas01mj@732: mas01mj@732: public function setContext(context:DisplayObjectContainer):void { mas01mj@732: this.context = context; mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Returns the visual DisplayObjectContainer that will be used by mas01mj@732: * addChild and removeChild helper methods. mas01mj@732: **/ mas01mj@732: public function getContext():DisplayObjectContainer { mas01mj@732: return context; mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Called from within setUp or the body of any test method. mas01mj@732: * mas01mj@732: * Any call to addAsync, will prevent test execution from continuing mas01mj@732: * until the duration (in milliseconds) is exceeded, or the function returned by addAsync mas01mj@732: * is called. addAsync can be called any number of times within a particular mas01mj@732: * test method, and will block execution until each handler has returned. mas01mj@732: * mas01mj@732: * Following is an example of how to use the addAsync feature: mas01mj@732: * mas01mj@732: * public function testDispatcher():void { mas01mj@732: * var dispatcher:IEventDispatcher = new EventDispatcher(); mas01mj@732: * // Subscribe to an event by sending the return value of addAsync: mas01mj@732: * dispatcher.addEventListener(Event.COMPLETE, addAsync(function(event:Event):void { mas01mj@732: * // Make assertions *inside* your async handler: mas01mj@732: * assertEquals(34, dispatcher.value); mas01mj@732: * })); mas01mj@732: * } mas01mj@732: * mas01mj@732: * mas01mj@732: * If you just want to verify that a particular event is triggered, you don't mas01mj@732: * need to provide a handler of your own, you can do the following: mas01mj@732: * mas01mj@732: * public function testDispatcher():void { mas01mj@732: * var dispatcher:IEventDispatcher = new EventDispatcher(); mas01mj@732: * dispatcher.addEventListener(Event.COMPLETE, addAsync()); mas01mj@732: * } mas01mj@732: * mas01mj@732: * mas01mj@732: * If you have a series of events that need to happen, you can generally add mas01mj@732: * the async handler to the last one. mas01mj@732: * mas01mj@732: * The main thing to remember is that any assertions that happen outside of the mas01mj@732: * initial thread of execution, must be inside of an addAsync block. mas01mj@732: **/ mas01mj@732: protected function addAsync(handler:Function = null, duration:Number=DEFAULT_TIMEOUT, failureHandler:Function=null):Function { mas01mj@732: if(handler == null) { mas01mj@732: handler = function(args:*):* {return;}; mas01mj@732: } mas01mj@732: var async:AsyncOperation = new AsyncOperation(this, handler, duration, failureHandler); mas01mj@732: asyncQueue.push(async); mas01mj@732: return async.getCallback(); mas01mj@732: } mas01mj@732: mas01mj@732: internal function asyncOperationTimeout(async:AsyncOperation, duration:Number, isError:Boolean=true):void { mas01mj@732: if(isError) getResult().addError(this, new IllegalOperationError("TestCase.timeout (" + duration + "ms) exceeded on an asynchronous operation.")); mas01mj@732: asyncOperationComplete(async); mas01mj@732: } mas01mj@732: mas01mj@732: internal function asyncOperationComplete(async:AsyncOperation):void{ mas01mj@732: // remove operation from queue mas01mj@732: var i:int = asyncQueue.indexOf(async); mas01mj@732: asyncQueue.splice(i,1); mas01mj@732: // if we still need to wait, return mas01mj@732: if(waitForAsync()) return; mas01mj@732: if(currentState == SET_UP) { mas01mj@732: runMethod(currentMethod); mas01mj@732: } mas01mj@732: else if(currentState == RUN_METHOD) { mas01mj@732: runTearDown(); mas01mj@732: } mas01mj@732: } mas01mj@732: mas01mj@732: private function waitForAsync():Boolean{ mas01mj@732: return asyncQueue.length > 0; mas01mj@732: } mas01mj@732: mas01mj@732: protected function runTearDown():void { mas01mj@732: if(currentState == TEAR_DOWN) { mas01mj@732: return; mas01mj@732: } mas01mj@732: currentState = TEAR_DOWN; mas01mj@732: if(isComplete) { mas01mj@732: return; mas01mj@732: } mas01mj@732: if(!runSingle) { mas01mj@732: getResult().endTestMethod(this, currentMethod); mas01mj@732: tearDown(); mas01mj@732: layoutManager.resetAll(); mas01mj@732: } mas01mj@732: setTimeout(runBare, 5); mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Helper method for testing DisplayObjects. mas01mj@732: * mas01mj@732: * This method allows you to more easily add and manage DisplayObject mas01mj@732: * instances in your TestCase. mas01mj@732: * mas01mj@732: * If you are using the regular TestRunner, you cannot add Flex classes. mas01mj@732: * mas01mj@732: * If you are using a FlexRunner base class, you can add either mas01mj@732: * regular DisplayObjects or IUIComponents. mas01mj@732: * mas01mj@732: * Usually, this method is called within setUp, and removeChild mas01mj@732: * is called from within tearDown. Using these methods, ensures that added mas01mj@732: * children will be subsequently removed, even when tests fail. mas01mj@732: * mas01mj@732: * Here is an example of the addChild method: mas01mj@732: * mas01mj@732: * private var instance:MyComponent; mas01mj@732: * mas01mj@732: * override protected function setUp():void { mas01mj@732: * super.setUp(); mas01mj@732: * instance = new MyComponent(); mas01mj@732: * instance.addEventListener(Event.COMPLETE, addAsync()); mas01mj@732: * addChild(instance); mas01mj@732: * } mas01mj@732: * mas01mj@732: * override protected function tearDown():void { mas01mj@732: * super.tearDown(); mas01mj@732: * removeChild(instance); mas01mj@732: * } mas01mj@732: * mas01mj@732: * public function testParam():void { mas01mj@732: * assertEquals(34, instance.value); mas01mj@732: * } mas01mj@732: * mas01mj@732: **/ mas01mj@732: protected function addChild(child:DisplayObject):DisplayObject { mas01mj@732: return getContext().addChild(child); mas01mj@732: } mas01mj@732: mas01mj@732: /** mas01mj@732: * Helper method for removing added DisplayObjects. mas01mj@732: * mas01mj@732: * Update: This method should no longer fail if the provided DisplayObject mas01mj@732: * has already been removed. mas01mj@732: **/ mas01mj@732: protected function removeChild(child:DisplayObject):DisplayObject { mas01mj@732: if(child == null) { mas01mj@732: return null; mas01mj@732: } mas01mj@732: try { mas01mj@732: return getContext().removeChild(child); mas01mj@732: } mas01mj@732: catch(e:Error) { mas01mj@732: } mas01mj@732: return null; mas01mj@732: } mas01mj@732: } mas01mj@732: }