I am not the best Action Script developer, in fact I had never written a single line of Action Script when I started at my current company. The language is quite similar to JavaScript however, so Java developers are able to learn Action Script rather fast. From the first chapters in the book I could learn some stuff about Flex that I didn't knew before. Also it woke my interest in asynchronous testing using Flexunit. Similar to the Webservice example from the book, our unit-tests are also using asynchronous service calls. Well these tests are not really unit but integration tests. The first thing that is done is to start a embedded jetty using Maven and the maven-jetty-plugin:
<build>
<plugins>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.26</version>
<configuration>
<stopKey>foo</stopKey>
<stopPort>9999</stopPort>
<contextPath>/</contextPath>
<webApp>
${settings.localRepository}/package/goes/here/1.0-SNAPSHOT/game-1.0-SNAPSHOT.war
</webApp>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>process-test-classes</phase>
<goals>
<goal>deploy-war</goal>
</goals>
<configuration>
<daemon>true</daemon>
</configuration>
</execution>
<execution>
<id>stopconfigurationReport-jetty</id>
<phase>prepare-package</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
It is a bit dirty that we are starting a WAR file straight out of the Maven repository using an absolute file path. Not sure there is a way to tell the maven-jetty-plugin to start a WAR file of a Maven dependency instead. This does the job for now.
When embedded Jetty is running, our Flexunit integration tests will fire a sequence of service calls against the server. The sequence starts with resetting the test user, then some calls altering the player data and finally a call to load the test user back from the server. For the test user loaded at the end of the test, we run some Flexunit assert statements to validate that we are in the expected state. Since each of the service calls in the test sequence must be fired asynchronously, we use Async.handleEvent to wait until a certain Event has occurred otherwise we fail the test. Async.handleEvent has a Function argument called eventHandler, which will be the Function that is invoked if the Event has been fired successfully. Given that your service call sequence has a few steps, the test quickly mutates into a chain of Function calls of private Functions within the same test class. If you are coming from a JUnit or TestNG background, these sort of tests are not very attractive. For another developer, just from reading the code, it might be really hard to follow the test flow. I found myself adding comments all over the place, documenting where the test is continuing and what we were testing.
Here is a typical test which I made up to give you an example:
public class AlterPlayerTest {
private static const TIMEOUT : int = 2000;
private static const INITIALIZED : String = "initialized";
private static const SAVED : String = "saved";
private static const LOADED : String = "loaded";
private static const FAILED : String = "failed";
private var eventDispatcher : EventDispatcher = new EventDispatcher();
private var rpcClient : RpcClient;
private var user : User = null;
public function AlterPlayerTest() { }
[Test(async)]
public function testAlteringUser() : void {
this.rpcClient = new RpcClient(ConnectionParameters.getValues());
// make sure the user is created on the server side
this.rpcClient.init(dispatchInitializeEvent, dispatchFailEvent);
Async.handleEvent(this, this.eventDispatcher, INITIALIZED, alterUser, TIMEOUT); // flow continues in alterUser function
}
private function alterUser(evt : Event, passThru : Object) : void {
var user : User = new User();
user.id = new NetworkId(NetworkId.FACEBOOK, "189891015686187");
// make sure all objects are gone
var removeAll : RemoveAllObjects = new RemoveAllObjects();
var changeBatch : ChangeBatch = new ChangeBatch(new ArrayCollection([ removeAll ]).source);
this.rpcClient.saveUserUsingChangeBatch(dispatchSavedEvent, dispatchFailEvent, user, changeBatch);
Async.handleEvent(this, this.eventDispatcher, SAVED, reloadChangedUser, TIMEOUT); // flow continues in the reloadChangedUser function
}
private function reloadChangedUser(evt : Event, passThru : Object) : void {
// load back the user and verify that the RemoveAllObjects has been applied
this.rpcClient.load(dispatchUserLoadedEvent, dispatchFailEvent);
Async.handleEvent(this, this, LOADED, assertChanges, TIMEOUT);
}
private function assertChanges(evt : Event, passThru : Object) : void {
var user : User = this.user;
Assert.assertNotNull("user should not be null", user);
var objects : WorldObjects = this.user.worldObjects;
Assert.assertTrue("user should not have any objects left but had " + objects.length, objects.length == 0);
}
private function dispatchInitializeEvent(... args) : void {
dispatchEvent(new Event(INITIALIZED));
}
private function dispatchSavedEvent(results : Array) : void {
dispatchEvent(new Event(SAVED));
}
private function dispatchUserLoadedEvent(user : User) : void {
this.user = user;
dispatchEvent(new Event(LOADED));
}
private function dispatchFailEvent() : void {
dispatchEvent(new Event("Failed"));
}
}
In the AlterPlayerTest the service call sequence has contains only 3 service calls. The initial Flexunit test method is called testAlteringUser from which rpcClient.init is invoked. The test will break if no INITIALIZED Event is fired within 2 seconds. If the INITIALIZED Event is dispatched properly, the test flow continues with the alterUser function in which the service call rpcClient.saveUserUsingChangeBatch is made. Here the test expects the SAVED Event or it will fail. Being successful, the test will finally continue in reloadChangedUser, where the last service call rpcClient.load is made. Here the test user is retrieved from the Java server backend and again, a LOADED Event is expected. At the end the test is asserting that the test user has been altered.
While this test might be fine, we often have test scenarios where different game players interact with each other and the service call sequence is 10+ calls long, which makes the test really hard to read.
Fortunately today I found a nice framework called Fluint Sequences which has been contributed and become part of Flexunit. As the name suggests, use this if you want to write a fluent sequence of service calls within your test case. So here is a rewrite of the test above using Fluint Sequences:
public class AlterPlayerSequentiallyTest {
private static const TIMEOUT : int = 2000;
private static const INITIALIZED : String = "initialized";
private static const SAVED : String = "saved";
private static const LOADED : String = "loaded";
private static const FAILED : String = "failed";
private var eventDispatcher : EventDispatcher = new EventDispatcher();
public function AlterPlayerSequentiallyTest() { }
[Test(async)]
public function testAlteringUser() : void {
var rpcClient = new RpcClient(ConnectionParameters.getValues());
var user : User = new User();
user.id = new NetworkId(NetworkId.FACEBOOK, "189891015686187");
var passThroughData : Object = new Object();
passThroughData.usr = null;
var sequence:SequenceRunner = new SequenceRunner(this);
// make sure the user is created on the server side
sequence.addStep(sequenceCaller(rpcClient.init,
[function() : void {
eventDispatcher.dispatchEvent(new Event(INITIALIZED));
}, dispatchFailEvent]
)
);
sequence.addStep(new SequenceWaiter(this.eventDispatcher, INITIALIZED, TIMEOUT));
// make sure all objects are gone
sequence.addStep(sequenceCaller(rpcClient.saveUserUsingChangeBatch,
[function() : void {
eventDispatcher.dispatchEvent(new Event(SAVED));
}, dispatchFailEvent, user, new ChangeBatch(new ArrayCollection([ new RemoveAllObjects() ]).source)]
)
);
sequence.addStep(new SequenceWaiter(this.eventDispatcher, SAVED, TIMEOUT));
// reload user and store in passThroughData
sequence.addStep(sequenceCaller(rpcClient.load,
[function(user : User) : void {
passThroughData.usr = user;
eventDispatcher.dispatchEvent(new Event(LOADED));
}, dispatchFailEvent]
)
);
sequence.addStep(new SequenceWaiter(this.eventDispatcher, LOADED, TIMEOUT));
// assert changes
sequence.addAssertHandler(assertChanges, passThroughData);
// start the test
sequence.run();
}
private function sequenceCaller(fnc : Function, args : Array) {
return new SequenceCaller(
this.eventDispatcher,
fnc,
args
)
}
private function assertChanges(evt : Event, passThroughData : Object) : void {
var user : User = passThroughData.usr;
Assert.assertNotNull("user should not be null", user);
var objects : WorldObjects = this.user.worldObjects;
Assert.assertTrue("user should not have any objects left but had " + objects.length, objects.length == 0);
}
private function dispatchFailEvent() : void {
dispatchEvent(new Event("Failed"));
}
}
This test is not at all shorter than the first one but I think it is much easier to read and understand. Also some private member fields could be changed to local variables. The bread and butter of this test is the SequenceRunner, which acts as a container for steps which should be executed in order. No step will be executed until you invoke SequenceRunner.run() however. The first step is a SequenceCaller wrapping the rpcClient.init call followed by a SequenceWaiter step which waits the specified time for the INITIALIZED Event to occur. I do the same thing for altering and reloading the user. Finally I define and execute a Function to contain my assertions.
0 Kommentare:
Kommentar veröffentlichen