[mtasc] as2lib unit testing and logging in Flashout...
martin heidegger
mastakaneda at gmail.com
Wed Aug 17 12:37:01 CEST 2005
I was writing at an article for the homepage that would help, but it
got destroyed during provider change ... i have got a draft (with
some/many mistakes in eighter formatting or content) on my harddisk in
.txt form. I attach it to the mail it could help you perhaps.
yours
Martin.
2005/8/17, lieven.cardoen at pointx.be <lieven.cardoen at pointx.be>:
> Well, ... it looks heavy stuff... but I'll dig in to it a little deeper.
> thx
>
> -----Original Message-----
> From: mtasc-bounces at lists.motion-twin.com
> [mailto:mtasc-bounces at lists.motion-twin.com] On Behalf Of martin
> heidegger
> Sent: woensdag 17 augustus 2005 11:04
> To: MotionTwin ActionScript2 Compiler List
> Subject: Re: [mtasc] as2lib unit testing and logging in Flashout...
>
> Its actually not that complex ...
>
> As most settings are accurate you only have to follow the
> configuration approach
> here:http://as2lib.org/documentation/articles/as2lib_configuration_api
>
> and create a custom configuration like:
>
> import org.as2lib.app.conf.AbstractConfiguration;
> import org.as2lib.app.conf.UnitTestExecution;
>
> class main.Configuration extends AbstractConfiguration {
>
> public static function init(Void):Void {
> initProcess(new UnitTestExecution());
> }
>
> private function setReferences(Void):Void {
> use(org.as2lib.app.exec.TTimeout); // reference your
> testcases here to include them @ compiletime
> }
> }
>
> If you want to use a different logger just create a different
> main.Mtasc configuration. And you can simply compile your testcases
> with mmc too.
>
> Too complex???
>
> yours
> Martin.
>
> 2005/8/17, lieven.cardoen at pointx.be <lieven.cardoen at pointx.be>:
> >
> >
> >
> > Does anybody know how to make as2lib unit testing log in flashout. I
> know
> > there's a class FlashoutLogger but how to use this...
> >
> >
> > --
> > MTASC : no more coffee break while compiling
> >
> >
> --
> MTASC : no more coffee break while compiling
>
> --
> MTASC : no more coffee break while compiling
>
-------------- next part --------------
The TestCase
The central thing of a xUnit System is a testcase. A testcase is a class that
tests another class. xUnit defines some "features" that help to write testcases
in a sure, easily readable way.
Lets take a example class for future descriptions. The following class could be
a class you started to think about. Its your first implementation, you don't know
what will happen to it, so its small and easy to understand (keep in mind that
classes are most times more complex!):
class com.domain.ui.Box {
private var width:Number;
private var heiht:Number;
public function setWidth(width:Number):Void {
this.width = width;
}
public function getWidth(Void):Number {
return width;
}
public function setHeight(height:Number) {
this.height = height;
}
public function getHeight(Void):Number {
return height;
}
}
If you now want to ensure that the box exactly returns what you expect (even in
future versions of your application you can write a testcase. If you don't use a
xUnit system it could look like:
import com.domain.ui.Box;
class com.domain.ui.TBox {
public function test() {
var box:Box;
var errorOccured:Boolean = false;
box = new Box();
box.setWidth(250);
box.setHeight(50);
if(box.getHeight() != 50) {
trace("Error occured: Box didn't contain the correct height: "+box.getHeight()+" != 50");
errorOccured = true;
}
if(box.getWidth() != 250) {
trace("Error occured: Box didn't contain the correct width: "+box.getWidth()+" != 250");
errorOccured = true;
}
if(!errorOccured) {
trace("Test run without any error");
}
}
}
You simple would have to run
new com.domain.ui.TBox().test();
to execute your test and get a result if the class works or not. In the current
case you would get:
Test run without any error
And if you create a error (like checking for 40 than for 50):
Error occured: Box didn't contain the correct height: 50 != 40
Of course the output is not that beautiful:
* It doesn't show you in what class it occured - this is a problem if your wrote
more than one or (more important) more than 20 tests. It wouldn't be possible
to find out where the error occured, or it would result in a lot of typing work)
* The routine to proove if the test really run perfectly has to be implemented
for each test
* If an error occured, you can't see what the condition was.
* The more informations you put to the output, the more will get redundant(not acceptable).
Okay, so far so good. Any xUnit system has a abstract class that can be extended
and that contains more or less standardized methods that can be used. The as2lib
has as abstract testcase: org.as2lib.test.unit.TestCase. Same test like above
would look with the as2lib (or most other systems) like:
import org.as2lib.test.unit.TestCase;
class com.domain.ui.BoxTestCase extends TestCase {
public function testBox() {
var box:Box;
box = new Box();
box.setWidth(250);
box.setHeight(50);
assertEquals("Box didn't contain the correct height", box.getHeight(), 50);
assertEquals("Box didn't contain the correct width", box.getWidth(), 250);
}
}
Lets explain it:
The system automatically searches all methods that begin with "test" from the
concrete class (your testcase) and executes them. As i've named the method
"testBox" it will start it.
"assertEquals" itself is a method of "TestCase". It compares two values and if
they don't match it displays the error message.
Following assertions are available in the as2lib testing system:
<table>
<thead>
<tr>
<th>Checking expression</th>
<th>Method name</th>
<th>Arguments</th>
</tr>
</thead>
<tbody>
<tr>
<th>value1 === value2</th>
<td>assertEquals</td>
<td>[Message:String], value1, value2</td>
</tr>
<tr>
<th>value1 !== value2</th>
<td>assertNotEquals</td>
<td>[Message:String], value1, value2</td>
</tr>
<tr>
<th>value1 == value2</th>
<td>assertSame</td>
<td>[Message:String], value1, value2</td>
</tr>
<tr>
<th>value1 != value2</th>
<td>assertNotSame</td>
<td>[Message:String], value1, value2</td>
</tr>
<tr>
<th>value === null</th>
<td>assertNull</td>
<td>[Message:String], value</td>
</tr>
<tr>
<th>value !== null</th>
<td>assertNotNull</td>
<td>[Message:String], value</td>
</tr>
<tr>
<th>value === undefined</th>
<td>assertUndefined</td>
<td>[Message:String], value</td>
</tr>
<tr>
<th>value !== undefined</th>
<td>assertNotUndefined</td>
<td>[Message:String], value</td>
</tr>
<tr>
<th>value == null</th>
<td>assertNull</td>
<td>[Message:String], value</td>
</tr>
<tr>
<th>(value1 < value2 && value1+diff > value2) || (value1 > value2 && value1-diff < value2)</th>
<td>assertAlmostEquals</td>
<td>[Message:String], value1:Number, value2:Number, [diff:Number, takes 0.0000001 by default]</td>
</tr>
<tr>
<th>value === Infinity</th>
<td>assertInfinity</td>
<td>[Message:String], value</td>
</tr>
<tr>
<th>value !== Infinity</th>
<td>assertNotInfinity</td>
<td>[Message:String], value</td>
</tr>
<tr>
<th>typeof object == type</th>
<td>assertTypeOf</td>
<td>[Message:String], object, type:String</td>
</tr>
<tr>
<th>object instanceof type</th>
<td>assertInstanceOf</td>
<td>[Message:String], object, type:Function</td>
</tr>
<tr>
<th>executable throws an (specific if applied) error</th>
<td>assertThrows</td>
<td>[Message:String], [Error], executable:Executable, arguments:Array / [Message:String], [Error], object, function:Function, arguments:Array / [Message:String], [Error], object, functionName:String, arguments:Array</td>
</tr>
<tr>
<th>executable does not throw an (specific if applied) error</th>
<td>assertNotThrows</td>
<td>[Message:String], [Error], executable:Executable, arguments:Array / [Message:String], [Error], object, function:Function, arguments:Array / [Message:String], [Error], object, functionName:String, arguments:Array</td>
</tr>
</tbody>
</table>
If you actually start the code (don't forget to setup the logging) with:
import org.as2lib.env.log.repository.DynamicLoggerRepository;
import org.as2lib.env.log.logger.TraceLogger;
import org.as2lib.test.unit.TestRunner;
import org.as2lib.test.unit.LoggerTestListener;
org.as2lib.env.log.LogManager.setLoggerRepository(new DynamicLoggerRepository(TraceLogger));
var testRunner:TestRunner = new TestRunner();
testRunner.addProcessListener(new LoggerTestListener());
testRunner.run(new com.domain.ui.BoxTestCase());
(The configuration will be simplified during later parts of the tutorial)
You will get the result in form of:
00:04:20.356 INFO org.as2lib.test.util.LoggerTestListener - TestRunner started execution.
00:04:20.396 INFO org.as2lib.test.util.LoggerTestListener - TestRunner finished with the result:
com.domain.ui.BoxTestCase run 1 methods in [3ms]. no error occured
If you would test for a wrong value it would look like:
00:06:16.904 INFO org.as2lib.test.util.LoggerTestListener - TestRunner started execution.
00:06:16.934 INFO org.as2lib.test.util.LoggerTestListener - TestRunner finished with the result:
com.domain.ui.BoxTestCase run 1 methods in [3ms]. 1 error occured
testBox() [3ms] 1 error occured
assertEquals failed with message: Box didn't contain the correct height!
50 != 40
The advantages are obvious: You see exactly where the error occured, you can see
what the exact error was and that with a pretty minimalistic code.
setUp & tearDown
----------------
The former testcase didn't seperate between the two methods ("setWidth", "setHeight"). A first idea to handle this could look like:
import org.as2lib.test.unit.TestCase;
import com.domain.ui.Box;
class com.domain.ui.BoxTestCase extends TestCase {
public function testWidth() {
var box:Box;
box = new Box();
box.setWidth(250);
assertEquals("Box didn't contain the correct width", box.getWidth(), 250);
}
public function testHeight() {
var box:Box;
box = new Box();
box.setHeight(50);
assertEquals("Box didn't contain the correct height", box.getHeight(), 40);
}
}
As both methods (testWidth&testHeight) begin with "test", both will be executed.
Like defined in xUnit: Each method execution will get its own, clean instance of
the testcase to avoid sideeffects by preset properties (or something like that).
As there are clean instances, the testsystem executes "setUp" before the execution
and "tearDown" after the execution. Those methods can be overwritten so that
every method gets the property:
import org.as2lib.test.unit.TestCase;
import com.domain.ui.Box;
class com.domain.ui.BoxTestCase extends TestCase {
private var box:Box;
public function setUp(Void):Void {
box = new Box();
}
public function tearDown(Void):Void {
delete box;
}
public function testWidth() {
box.setWidth(250);
assertEquals("Box didn't contain the correct width", box.getWidth(), 250);
}
public function testHeight() {
box.setHeight(50);
assertEquals("Box didn't contain the correct height", box.getHeight(), 40);
}
}
This is specially important if you have to close a connection or if you change
any global configuration. In the current case it helped to seperated between
width and height.
Important! Its not necessary that the constructor of the class will ever get
executed, so any code in the constructor would be useless.
Support of lazy operations
--------------------------
As lazy operations are one of the major problems in ActionScript (and they seem
to stay) its important that the TestSystem allows testing lazy operations. A usual
lazy operation has to handle loading of files or to analyse framebased operations.
There are two ways how to work with lacy operations within a testcase:
1.) Using pause & resume.
TestCase has the methods pause & resume. They allow to stop the testcase
execution and resume its work. Lets take a look at a example for how to use
them:
import org.as2lib.test.unit.TestCase;
class com.domain.ui.LoadingTestCase extends TestCase {
public function testBoxByPropFile(Void):Void {
var file:XML = new XML();
var that = this;
file.onLoad = function() {
that["finishTestBoxByPropFile"](this);
}
pause();
file.load("propfile.xml");
}
private function finishTestBoxByPropFile(file:XML) {
// Analysation of the file
resume();
}
}
The important part of the example is that .pause has to be executed
before the lazy process gets executed (it might be possible that .onLoad
gets really executed before pause then you have a paused system. And you
have to ensure that .resume gets called at the end of the last method related
to it, and it should get executed _for sure_ else the testrunner will not
continue its work.
2.) Using of Process abilities
The execution package (to be found at org.as2lib.app.exec) contains a
definition and serveral implementation to work with lazy application parts.
TestCase allows to start a process and stops during its execution. A example
for a lazy integration in a testcase could look like:
import org.as2lib.app.exec.AbstractProcess;
class com.domain.ui.FileLoadingProcess extends AbstractProcess {
private var file:XML;
private var uri:String;
public function FileLoadingProcess(uri:String) {
file = new XML();
this.uri = uri;
}
public function getFile(Void):XML {
return file;
}
public function run(Void):Void {
var owner = this;
file.onLoad = function() {
owner.resume();
owner.finish();
}
pause();
file.load(uri);
}
}
import org.as2lib.test.unit.TestCase;
import org.as2lib.app.exec.Call;
import com.domain.ui.FileLoadingProcess;
class com.domain.ui.LoadingTestCase extends TestCase {
private var loadingProcess:FileLoadingProcess;
public function testBoxByPropFile(Void):Void {
loadingProcess = new FileLoadingProcess("prop.xml");
startProcess(loadingProcess, [], new Call(this, finishTestBoxByPropFile));
}
private function finishTestBoxByPropFile() {
var file:XML = fileLoadingProcess.getFile();
// Analysation of the file
}
}
As FileLoadingProcess is not part of the as2lib yet it takes a little more
time to have enough infrastructure. But this approach only has to watch that
startProcess gets executed at the end of a method operation (to not confuse
the testrunner).
The TestRunner
--------------
The TestRunner ist the main actor using testcases. It analyses the applied
testcase(s) and executes it. As you might have recognized it has been used within
the small former configuration.
<quote>
var testRunner:TestRunner = new TestRunner();
testRunner.addProcessListener(new LoggerTestListener());
testRunner.run(new com.domain.ui.BoxTestCase());
</quote>
As the testcase should not know anything about its execution, all informations
about the current testcase execution get managed by the testrunner (this is not
really described in xUnit, so not all or even most systems handle it concrete
like that).
It is possible (for in-the-depth working) to define listeners to the TestRunner
for visual output or detailed informations. The as2lib supports with
"org.as2lib.test.unit.LoggerTestListener" a default listener to
write the well readable result directly to the defined logger.
If you speak in MVC, the TestRunner would be the Controller and the Model could
be found at TestRunner.getTestResult(Void):org.as2lib.test.unit.TestResult. Any
listener added with .addProcessListener() could act as View.
The TestSuite
-------------
A TestSuite is per definition a collection of Testcases. If you have bigger parts
of your application that shall get grouped you can use TestSuites. You can simply
create a TestSuite like this:
import org.as2lib.test.unit.TestSuite;
var testSuite:TestSuite = new TestSuite("My TestSuite");
testSuite.addTest(new com.domain.ui.BoxTestCase());
As both (TestCase & TestSuite) implement the interface org.as2lib.test.unit.Test
(which is required in TestSuite.addTest(test:Test);) its possible to even add
TestSuites to TestSuites:
import org.as2lib.test.unit.TestSuite;
var testSuite:TestSuite = new TestSuite("My own TestSuite");
var testSuite2:TestSuite = new TestSuite("My private TestSuite");
testSuite2.addTest(new com.domain.ui.BoxTestCase());
testSuite.addTest(new com.domain.ui.BoxTestCase());
testSuite.addTest(testSuite2);
The as2lib supports with org.as2lib.test.unit.TestSuiteFactory a easy way to collect
all testcases that are available at runtime:
import org.as2lib.test.unit.TestSuiteFactory;
import org.as2lib.test.unit.TestSuite;
var testSuite:TestSuite = new TestSuiteFactory().collectAllTestCases();
Easy Configuration
------------------
The current configuration as used above is some kind of unusable to your take it
for projects. We use the most simple approach possible with flash.
The class org.as2lib.app.conf.TestExecution allows access to easy integration.
If you customize your main.Configuration file (configuration like described in
our article) it allows almost the easiest possible way to execute testcases by
simply adding the certain testcase with use(), here is a example:
import org.as2lib.app.conf.AbstractConfiguration;
import org.as2lib.app.conf.TestExecution;
class main.Configuration extends AbstractConfiguration {
public static function init(Void):Void {
initProcess(new TestExecution());
}
private function setReferences(Void):Void {
use(com.domain.ui.BoxTestCase);
use(com.domain.ui.LoadingTestCase);
}
}
Resume
------
More information about the mtasc
mailing list