Understanding Rules and their uses

A Special Note

::''With great power comes great responsibility.'' ~Ben Parker

Rules are an advanced concept whose basic functionality is similar to the [Before] and [After] concepts, allowing for reusable, dynamic classes that will execute functionality before and after each test. If you are unfamiliar with the use of [Before] and [After] it is highly recommend that you take a look at these tools before delving into Rules.

It should be noted that while rules allow for a great deal of framework flexibility, it is very easy to write improperly-coded rules that may cause none of your tests to run. Use with caution.

Rules Defined

Frequently during test writing, a developer wants to add code to their tests that will run before/after every single test they write. Most pre- and post-test code can be defined in a [Before] or [After] method and use of these methods is normally sufficient. However, sometimes a developer finds it necessary to write more advanced functionality.

This is where Rules come in. A Rule allows a developer to inject additional functionality into a test Runner without actually writing a custom runner. It allows for asynchronous Before and After setups/teardowns as well as advanced method running prior to the actual run case of each test. Similar to the Before and After metadata, a Rule runs before and after each test. It is possible to write a rule that includes before functionality, after, or both.

Additionally, since rules persist through the duration of any given test, you may even add functionality to your rule that is meant to be called within the test itself, say, passing in a test component to the rule during the test, which is meant to be evaluated in some way once the test has completed.

Creating a Rule

  • Inside of your test runner project, browse to the sampleSuite/tests directory and create a new package named 'rules'
  • Right click select New -> Actionscript Class named SimpleRule.as (this will be your rule class). This class should extend MethodRuleBase.as and implement the IMethodRule interface. Advanced users may instead implement IMethodRule and IAsyncStatement.
  • Import the IMethodRule, MethodRuleBase, AsyncTestToken and ChildResult classes.
         import org.flexunit.internals.runners.statements.MethodRuleBase;
         import org.flexunit.rules.IMethodRule;
    
         import org.flexunit.token.AsyncTestToken;
         import org.flexunit.token.ChildResult;
    
  • Override the evaluate() function from MethodRuleBase. Be sure to add the super call. After the super call, you may insert any code you would like to run ''before'' the actual test runs. In this case we are simply running a trace which will print to the console "Before Rule". This is of course very basic functionality, we could instead create a log here, a server call to retrieve data, a wait timer or any other functionality we desire prior to test execution. Note the use of proceedToNextStatement. Execution of this method notifies the runner that the before section of the rule is complete. You must ensure that this method somehow gets called (directly or indirectly) from evaluate() or else the runner will stop running, since this is how the runner is notified that the "before" part of the Rule has completed execution. Moreover, any code following this statement may not have the expected effect.
         override public function evaluate( parentToken:AsyncTestToken ):void {
              super.evaluate( parentToken );
        
              //Insert your code here.
              trace( "Before Rule" );
                
              proceedToNextStatement();
         }
    
  • Now override the handleStatementComplete() function from MethodRuleBase. This method will act as an after method and will run all code contained within. Note the location of the super call. A call to the super must be made in order to complete execution. Do ''not'' make this call until you are sure all your after code is complete. Like proceedToNextStatement in evaluate(), calling the super here informs the runner that the rule has completed execution. Not calling it will cause the runner to stop running, and calling it too soon may cause unexpected behavior.
         override protected function handleStatementComplete( result:ChildResult ):void {
              //Insert your code here.
              trace( "After Rule" );
                
              super.handleStatementComplete( result );
         }
    
  • Create a new test case. For information on creating a new test case see Writing a basic test.
  • Import the SimpleRule.as class.
         import rules.SimpleRule;
    
  • Create a new public variable called ruleOne of type SimpleRule. Instantiate the rule. Attach the [Rule] metadata to this variable. This will instantiate the rule with the class and cause it to run before/after every test depending on your implementation.
         [Rule]
         public var ruleOne:SimpleRule = new SimpleRule();
    
  • Create some simple test cases, add the test to your test suite and run it. You should see your rule handled before and after each test.

Rules Place in the Framework

Since rules act in a way similar to the [Before] and [After] metadata, it is important to understand in what order they are processed. Rules act as a wrapper to each test case, and can execute code before all [Before]s and after all [After]. Unless an order has been explicitly specified in the metadata, they are handled in no particular order. An example hierarchy is as follows:
:-BeforeClasses
::-Rules
::-Befores
:::-Test
::-Afters
::-Rules (the same ones as above)
:-AfterClasses

Adding Order to your Rules

Obviously, having no control over the order of your rules execution can cause a bit of an issue. Thankfully, we can add order to the metadata to control rule execution order.

     [Rule(order=1)]
     public var ruleOne : SimpleRuleOne = new SimpleRuleOne();

     [Rule(order=3)]
     public var ruleTwo : SimpleRuleTwo = new SimpleRuleTwo();

     [Rule(order=2)]
     public var ruleThree : SimpleRuleThree = new SimpleRuleThree();

This code will cause ruleOne to execute, then ruleThree and finally ruleTwo.

Advanced Topic - Rules as Decorators

As has been alluded to at various points, Rules are not exactly re-usable versions of [Before] and [After]. Rather, they are "decorators," or wrappers around any test method plus their associated [Before]s and [After]s. This means that you have a lot of flexibility in their usage. You can, for example, have a Rule that is meant to be interacted with by the test method itself, say, by setting certain properties on the rule or calling certain methods. Or you can have a Rule that continuously captures profiling information in the background while the test runs, sending that information to a LocalConnection() to be captured and parsed by a listener application. Furthermore, since a Rule's apply() method has information on both the test method (the method parameter) and the test case class (the test parameter), you may have your rule interact with or be responsive to these as well. For example, your rule could inspect the metadata on the test method and conditionally apply functionality based on existent metadata information.

In other words, Rules, as opposed to [Before] and [After], have far more possibilities in their usage.

Advanced Rules

These basic rules setups work very well for simple code such as the trace statement above. However, what if I need to make an independent service call with every test across multiple classes? Obviously we could create tests that ran this code each time, but this is superfluous code reproduction. Instead, lets use a rule to make the service call and inject the return data.

  • Create a new rule using the code above. Call it InjectionRule.
  • In addition to the previous overrides, you will need to add an additional override for the apply method. In that override, save class info as a class variable.
         private var test:Object;
         override public function apply(base:IAsyncStatement, method:FrameworkMethod, test:Object):IAsyncStatement {
              // save the test class here.
              this.test = test;
    
              return super.apply( base, method, test );
         }
    
  • Now that we have access to the class we can inject data into it. Be sure that any data you are injecting into the test class actually exists. If the property does not exist, a run time error will be thrown.
  • As a slight alteration to the original rule, inject the data into the test class
         override public function evaluate( parentToken:AsyncTestToken ):void {
              super.evaluate( parentToken );
        
              //Insert your code here.
              test.injectedData = "Add Me";
                
              proceedToNextStatement();
         }
    
    You now have a simple injection rule. Obviously we could have used a service call here and added any return data to the class before calling proceedToNextStatement. This is only a rough example of the possibilities.
  • No labels