-
Notifications
You must be signed in to change notification settings - Fork 28
/
TestCase.as
499 lines (467 loc) · 14.7 KB
/
TestCase.as
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
package asunit.framework {
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.errors.IllegalOperationError;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.utils.describeType;
import flash.utils.getDefinitionByName;
import flash.utils.setTimeout;
import flash.utils.Timer;
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;
protected var timer:Timer;
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 = [];
timer = new Timer(0, 1);
timer.addEventListener(TimerEvent.TIMER, onTick);
}
// Not subscribing runBare() directly because that would require the Test interface to change.
protected function onTick(e:TimerEvent):void {
runBare();
}
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();
}
// Use Timer instead of setTimeout to reduce the failure call stack.
timer.reset()
timer.start();
}
/**
* 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;
}
}
}