From: Drew
Subject: Re: Loom and mock-as3
Date: February 18, 2009 7:39:07 AM EST
To: Maxim
Hi Max,
The idea about using closures to replace existing methods is much more
accessible as a solution then what I was thinking to do. I'll include
an example of how I was thinking to go later in this email.
I figured the best way to understand how I would use Loom for mocks
was to create a couple of use cases. The three scenarios I came up
with were:
- simple AOP: around, before, after;
- stubbing / partial mocking: replacing a method in the class under
test;
- mocking: using loom internally to create mock objects;
Have a look at the attached
loom-use-cases.as file for an example of
each scenario. I use an RSpec-inspired testing framework, and have
made assumptions about the Loom API. In case its not clear the
beforeAll functions are run once per describe block, hopefully
everything else in there makes sense.
One of possible pain points that I can see are that when using a
Loom-augmented class it would need to be typed as the target class for
use and not as the augmented type as the combined type will not be
available at compile time. Though I would be happy with the situation
below as it keeps the concerns (real use vs. augmentation) separate.
// we have to type to EventDispatcher to use it anywhere
var eventDispatcher:EventDispatcher = new
loomAugmentedEventDispatcherClass();
// but we cannot access any additional methods unless we type it to an
interface provided by Loom
var loomAugmented:LoomAugmented = eventDispatcher as LoomAugmented;
loomAugmented.around("dispatchEvent", function(fn:Function,
args:Array):Boolean {
// ...
});
// nor could we cast it like this, because
LoomAugmentedEventDispatcher would not exist as a type at compile
var loomAugmentedEventDispatcher:LoomAugmentedEventDispatcher =
eventDispatcher as LoomAugmentedEventDispatcher;
The asynchronous nature of generating and loading the bytes is easily
worked with too. Most developers are used to events and callbacks by
now so either style of async API is cool.
Can you explain a bit more about the use case you describe at the
start of your email? Is it so you can replace an existing class with
an augmented version? Eg replace class EchoService with class
EchoService implements LoomAugmentation, so none of the references to
that class have to point to the generated version?
Implementing the snippet in
abc-dsl.as is what I was toying with, its
goal was to create the class definition and bytecode that is currently
used by mock-as3. The point of using closures for each block is to
enable the setup of context and capture of values for the constant
pool, etc.
cheers,
Drew
=======
abc-dsl.as =========
var abc:ABC = __abc(function():void {
__package("example.mock", function():void {
// class name, class to extend, interfaces to implement
__class("WafflesMock", null, [Waffles], function():void {
__var("mock", Mock, NS.PUBLIC));
// iterate over methods we are going to implement
classInfo(Waffles).publicMethods.forEach(function
(method:Method):void {
__method(
method.name, method.parameters, method.returnType,
function():void {
__getproperty("mock");
__callmethod(
method.name, method.parameters.length);
if (method.returnType) {
__returnvalue();
} else {
__returnvoid();
}
});
});
// add an extra method because we can
__method("ifexample", function():void {
__ifeq(__getproperty("lhs"), __getproperty("rhs"), function
():void {
__getproperty("other");
__callmethod("example");
});
__returnvoid();
}));
}); // end class
}); // end package
}); // end abc
===========
loom-use-cases.as ===============
// AOP
describe('aop', function():void {
var loom:Loom;
var eventDispatcherClass:Class;
var eventDispatcher:EventDispatcher;
beforeAll(function():void {
loom = new Loom();
loom.weave(EventDispatcher, async(function(klass:Class):void {
eventDispatcherClass = klass;
}));
});
before(function():void {
eventDispatcher = new eventDispatcherClass();
});
describe('around', function():void {
it('receives a closure to execute the original method, and any
arguments', function():void {
loom.around(eventDispatcher, 'dispatchEvent', function
(fn:Function, args:Array):Boolean {
trace('eventDispatcher dispatchEvent around before', event);
var result:Boolean = fn.apply(null, args);
trace('eventDispatcher dispatchEvent around after', event);
return result;
});
});
});
describe('before', function():void {
it('receives arguments only', function():void {
loom.before(eventDispatcher, 'dispatchEvent', function
(args:Array):void {
trace('eventDispatcher dispatchEvent before', args);
});
});
});
describe('after', function():void {
it('receives arguments and result', function(args:Array,
result:Boolean):void {
loom.after(eventDispatcher, 'dispatchEvent', function
(args:Array, result:Boolean):void {
trace('eventDispatcher dispatchEvent after', args, result);
});
});
});
});
// stubbing / partial mock
describe('using a stub', function():void {
var swfReaderClass:Class;
var swfReader:SWFReader
var loom:Loom;
beforeAll(function():void {
loom = new Loom();
loom.weave(SWFReader, async(function(klass:Class):void {
swfReaderClass = klass;
}));
});
before(function():void {
swfReader = new swfReaderClass();
});
it('should allow partial mocking', function():void {
loom.replace(swfReader, 'readU30', function():int {
return 0xF430;
});
assertThat(swfReader.readU30(), equalTo(0xF430));
});
});
// mocking
describe('using a mock', function():void {
var urlLoaderClass:Class;
var urlLoader:URLLoader;
var swfReader:SWFReader;
var mockery:Mockery;
beforeAll(function():void {
mockery = new Mockery();
mockery.addEventListener('ready', async(done));
mockery.bake(URLLoader);
});
before(function():void {
urlLoaderMock = mockery.make(URLLoader);
swfReader = new SWFReader();
});
it('should have a target of the mocked/weaved class', function
():void {
urlLoaderMock.method('load').withArgs(URLRequest).dispatchesEvent
(new Event('complete'));
swfReader.urlLoader = urlLoaderMock.target as URLLoader;
swfReader.load(new URLRequest('path/to/swf'));
});
});