Unit 4 - FlexUnit Basics

Download Download Unit Project Files

FlexUnit 4.x tests are simply methods that are decorated with special metadata. They setup conditions, execute code and make assertions about expected results.

These methods are grouped together by common themes and executed often to ensure consistent, functional code.

Objectives:

After completing this lesson, you should be able to:

Topics

In this unit, you will learn about the following topics:

Testing a Unit

Understanding a Unit

A unit is the smallest block of code that can be isolated and tested independently.

Suppose you had a table lamp that was not working properly. You might first try changing the light bulb to see if that fixes the issue. Perhaps you would try that light bulb in another fixture or move the lamp to another room to try another outlet. These are all steps to isolate a problem and realizations of the idea behind testing units.

When the lamp is fully assembled with light bulb in place and plugged into the wall it is not possible to definitively see where an issue might exist. There could be an issue with the outlet, cord, switch, wiring or bulb. You can only begin to determine the problem by isolating one system at a time and testing it. If you could not change the light bulb, move the lamp, or disassemble it, your ability to test would only be at the whole system level. This likely means you throw away your lamp each time the bulb burns out.

While a lamp is a relatively simple example, the average class in your application is likely not. To be able to test a class properly, you need to ensure it is capable of being isolated and then test each piece in an isolated fashion.

To successfully unit test a class, you will:

This is only possible if you can test one isolated object at a time.

The Need for Seams

We call the point where objects can be separated from each other seams. Continuing with the lamp example, the light bulb socket and wall plug are both seams. Seams allow us to isolate an object and ensure that we are only testing one object at a time.

Let's examine the following piece of code:

public class TestSeams {
    private var someMathObj:SomeMathClass = new SomeMathClass();

public function findDistance( a:Number, b:Number ):Number {
    var difference:Number = ( b - a );
    var distance:Number = someMathObj.abs( difference );
    return distance;
}

}

Unfortunately, you cannot test the findDistance() method alone. That method depends upon SomeMathClass and its abs() function. Therefore, it cannot be isolated. Any attempt to test this method results in testing the code in the TestSeams class, an unknown amount in the SomeMathClass, and the integration between the classes.

When creating new code you must keep the need for seams in mind. Not only does it facilitate testable code, it also has the side effect of creating loosely coupled code with a minimum of interwoven dependencies.

Examining the Circle Class and the Circle Layout

Let's take a look at the Circle class and the getPointOnCircle() method.

public function getPointOnCircle( t:Number ):Point {
    var x:Number = origin.x + ( radius * Math.cos( t ) );
    var y:Number = origin.y + ( radius * Math.sin( t ) );

return new Point( x, y );

}

In this unit of code:

A unit test for getPointOnCircle() would need to test:

As you can see, most units will need more than one test to fully validate the unit's function.

Here is a unit from the CircleLayout class:

public function getLayoutRadius( contentWidth:Number, contentHeight:Number ):Number {
    var maxX:Number = (contentWidth/2).8;
    var maxY:Number = (contentHeight/2).8;
    var radius:Number = Math.min( maxX, maxY );

return Math.max( radius, 1 );

}

While not perfect, this unit has a couple testing advantages over the previous method:

A unit test for this method would need to test:

Examining a Test Case

As you learned in the previous unit, a test case is merely a collection of individual test methods that test related behaviors. Each of the methods of a test case is decorated with [Test] metadata to signal to the FlexUnit framework that a method is a test. The [Test] metadata tag can also accept user defined attributes for descriptive and organizational purposes

[Test(description="Test is supposed to fail",issueID="0012443")]

public function thisTestFails():void {}

or system-defined attributes that provide further instructions to FlexUnit when it runs the test.

A test fixture comprises all of the state required to evaluate your test methods. For example, in the following test:

[Test]
public function shouldComputeCorrectDiameter():void {
    var circle:Circle = new Circle( new Point( 0, 0 ), 5 ); 
    assertEquals( 10, circle.diameter );
}

The existence of the Circle is necessary for the assertion and can therefore be considered part of your test fixture.

Walkthrough 1: Adding Tests to a Case

In this walkthrough you will perform the following tasks:

Steps

  1. Open the BasicCircleTest.as file from the previous exercise.

    Alternatively, if you didn't complete the previous lesson or your code is not functioning properly, you can import the FlexUnit4Training_wt1.fxp project from the Unit 4/Start folder. Please refer to Unit 2: Walkthrough 1 for instructions on importing a Flash Builder project.

  2. Add seven new public functions to the class right under the shouldReturnProvidedRadius() function. They should be named shouldComputeCorrectDiameter(), shouldReturnProvidedOrigin(), shouldReturnTrueForEqualCircle(), shouldReturnFalseForUnequalOrigin(), shouldReturnFalseForUnequalRadius(), shouldGetPointsOnCircle(), and shouldThrowRangeError().

    Mark each function with [Test] metadata.

    [Test]
    public function shouldComputeCorrectDiameter():void {
    }
    
    [Test]
    public function shouldReturnProvidedOrigin():void {
    }
    
    [Test]
    public function shouldReturnTrueForEqualCircle():void { 
    }
    
    [Test]
    public function shouldReturnFalseForUnequalOrigin():void {
    }
    
    [Test]
    public function shouldReturnFalseForUnequalRadius():void {
    }
    
    [Test]
    public function shouldGetPointsOnCircle():void {    
    }
    
    [Test]
    public function shouldThrowRangeError():void {  
    }       
  3. Although you have written out seven methods, only the first five are going to be used in this walkthrough. Within shouldComputeCorrectDiameter(), shouldReturnProvidedOrigin(), shouldReturnTrueForEqualCircle(), shouldReturnFalseForUnequalOrigin(), and shouldReturnFalseForUnequalRadius() declare a local variable named circle of type Circle. Instantiate circle with an origin Point at (0, 0) and a radius of 5.

    [Test]
    public function shouldComputeCorrectDiameter():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 ); 
    }
    
    [Test]
    public function shouldReturnProvidedOrigin():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 ); 
    }
    
    [Test]
    public function shouldReturnTrueForEqualCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 ); 
    }
    
    [Test]
    public function shouldReturnFalseForUnequalOrigin():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 ); 
    }
    
    [Test]
    public function shouldReturnFalseForUnequalRadius():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 ); 
    }       
  4. Just below the circle instantiation in the shouldComputeCorrectDiameter() method, add a line that calls to the assertEquals() method with arguments 10 and circle.diameter.

    [Test]
    public function shouldComputeCorrectDiameter ():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 ); 
        assertEquals( 10, circle.diameter );
    }       
  5. Just below the circle instantiation in the shouldReturnProvidedOrigin() method, add two lines that each call the assertEquals() method. This test requires two assertion statements because it will check the x and y coordinates' equality to 0, each on a different line.

    [Test]
    public function shouldReturnProvidedOrigin():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 ); 
        assertEquals( 0, circle.origin.x );
        assertEquals( 0, circle.origin.y );
    }       
  6. Add a new local variable named circle2 of type Circle to the shouldReturnTrueForEqualCircle() method. Instantiate circle2 with an origin of (0, 0) and radius of 5.

    [Test]
    public function shouldReturnTrueForEqualCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var circle2:Circle = new Circle( new Point( 0, 0 ), 5 );
    }       
  7. Add a new line to the shouldReturnTrueForEqualCircle() function that calls to the assertTrue() method with the argument circle.equals( circle2 ).

    [Test]
    public function shouldReturnTrueForEqualCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var circle2:Circle = new Circle( new Point( 0, 0 ), 5 );
    
        assertTrue( circle.equals( circle2 ) ); 
    }       

    If you did not use code-completion, add the import for org.flexunit.asserts.assertTrue at this time.

  8. Add a variable named circle2 of type Circle to the shouldReturnFalseForUnequalOrigin() method. Instantiate circle2 with an origin of (0, 5) and a radius of 5.

    [Test]
    public function shouldReturnFalseForUnequalOrigin():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var circle2:Circle = new Circle( new Point( 0, 5 ), 5 );
    }       
  9. Add a call to the assertFalse() method. The statement circle.equals( circle2 ) should be passed in as its argument.

    [Test]
    public function shouldReturnFalseForUnequalOrigin():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var circle2:Circle = new Circle( new Point( 0, 5 ), 5);
    
        assertFalse( circle.equals( circle2 ) );
    }       

    If you did not use code-completion, add the import for org.flexunit.asserts.assertFalse at this point.

  10. Add a variable named circle2 of type Circle to the shouldReturnFalseForUnequalRadius() method. Instantiate circle2 with an origin of (0, 0) and a radius of 7.

    [Test]
    public function shouldReturnFalseForUnequalRadius():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var circle2:Circle = new Circle( new Point( 0, 0 ), 7);
    }       
  11. Add a call to the assertFalse() method. The expression circle.equals( circle2 ) should be passed in as its argument.

    [Test]
    public function shouldReturnFalseForUnequalRadius():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var circle2:Circle = new Circle( new Point( 0, 0 ), 7);
    
        assertFalse( circle.equals( circle2 ) );
    }       
  12. Save the BasicCircleTest.as file.

  13. Re-Open the FlexUnit4Training.mxml file. Click the run button in the upper toolbar.

    If FlexUnit4Training.mxml ran successfully you should see the following output in your browser window:

    UnitsPassed

    Figure 1: FlexUnit tests passed.

Creating tests

Examining a passing test

In the section above you used assertions to establish several passing tests. Many took the same form as this simple math example:

[Test]
public function testAddition():void {
    var num1:Number = 5;
    var num2:Number = 3;
    var total:Number = 8;

assertEquals( total, num1 + num2 );

}

Performing basic assertions

Assertions allow us to specify expected conditions that help determine whether a test passes or fails. They are statements that evaluate to true or false. If you make an assertion that is false under the conditions you have set up, the test fails. If all of the assertions in the test are true, the test passes.

FlexUnit provides many assertion methods to use in your test methods. You might use an assertion to state that you expect a value to be true or false, null or not null, equal to another variable, or simply not in a list of acceptable values.

The three most common assertions in FlexUnit 4 are:

If all of the assertion in a test evaluate correctly, that test is added to the list of passed tests. If any assertion fails, it throws an AssertionError. FlexUnit handles the error and adds the test to a list of failed tests.

Examining a failed test

Consider this slightly modified testAddition() method:

[Test]
public function testAddition():void {
    var num1:Number = 5;
    var num2:Number = 3;
    var total:Number = 1000;

assertEquals( total, num1 + num2 );

}

FlexUnit 4.x catches the AssertionError that is thrown when this test fails, and the following result is displayed within the FlexUnit Results window in Flash Builder:

"expected: <1000> but was: <8>"

A test will be registered as a failure after the first instance of a failed assertion. FlexUnit will not continue to evaluate the other assertions in the method. This means that if multiple assertions are written in a single test you will only receive notification of the first failure and the others will be in an unknown state.

[Test]
public function testAddition():void {
        var num1:Number = 5;
        var num2:Number = 3;
        var total:Number = 1000;

    assertEquals( total, num1 + num2 );
    assertEquals( num1 + num2, total );

}

For example, both of these assertions would fail given the opportunity. However, as soon as FlexUnit catches the first failed assertion it stops the test. The second assert will never be called. The error message would read:

"expected: <1000> but was: <8>"

With multiple assertions in a test, a test can fail for one of several reasons. In diagnosis, it is not immediately clear what caused the test to fail.

Walkthrough 2: Failing and Passing Assertions

In this walkthrough you will perform the following tasks:

Steps

  1. Open the BasicCircleTest.as file from the previous exercise.

    Alternatively, if you didn't complete the previous lesson or your code is not functioning properly, you can import the FlexUnit4Training_wt2.fxp project from the Unit 4/Start folder. Please refer to Unit 2: Walkthrough 1 for instructions on importing a Flash Builder project.

    Write multiple assertions for a test method

  2. In the shouldGetPointsOnCircle() method, create a local variable named circle of type Circle. This Circle should be instantiated with an origin of (0, 0) and a radius of 5.

    [Test]
    public function shouldGetPointsOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
    }       
  3. Declare a variable named point of type Point.

    [Test]
    public function shouldGetPointsOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point;
    }       
  4. Set the point variable equal to the return value of circle.getPointOnCircle( 0 ). Above that line, it is useful to add a comment line specifying that this is the top-most point of circle.

    [Test]
    public function shouldGetPointsOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point;
    
        //top-most point of circle
        point = circle.getPointOnCircle( 0 );
    }       

    The getPointOnCircle( t:Number ) returns a Circle object's point that corresponds to the radians passed in as the t parameter.

  5. Add two new lines, each with a call to the assertEquals() method. The first takes the arguments 5 and point.x. The second takes the arguments 0 and point.y.

    [Test]
    public function shouldGetPointsOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point;
    
        //top-most point of circle
        point = circle.getPointOnCircle( 0 );
        assertEquals( 5, point.x );
        assertEquals( 0, point.y );
    }       
  6. Copy these four new lines of code and paste them into the bottom of the function.

    Alter the comment in this section to read //bottom-most point on circle.

  7. Replace the 0 in circle.getPointOnCircle( 0 ) with Math.PI.

  8. Update the assertEquals() statements to assertEquals( -5, point.x ) and assertEquals( 0, point.y ).

    This should create a duplicate set of assertions for the bottom-most point on the circle.

    [Test]
    public function shouldGetPointsOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point;
    
        //top-most point of circle
        point = circle.getPointOnCircle( 0 );
        assertEquals( 5, point.x );
        assertEquals( 0, point.y );
    
        //bottom-most point of circle
        point = circle.getPointOnCircle( Math.PI );
        assertEquals( -5, point.x );
        assertEquals( 0, point.y );
    }       
  9. Save BasicCircleTest.as.

  10. Run the FlexUnit4Training.mxml file.

    If FlexUnit4Training.mxml ran successfully you should see the following output in your browser window:

    SingleTestFailure

    Figure 1: A single test failure

    One of the tests in the case has failed. Further evaluation within the FlexUnit results tab will show that the newly populated shouldGetPointsOnCircle() method failed.

    While you know that this test failed, you do not know the failure. Fewer assertions in a method provide you better context to understand the root cause quickly.

    If you examine the failure in Flash Builder, you will see that the root cause actually has to do with the imprecision of floating point calculations. You will learn to adapt your code to these issues in future lessons.

    FailureStackTrace

    Figure 2: The failure stack trace

Understanding multiple assertions

As you have previously learned, FlexUnit 4.x marks a test as a failure after the first failed assertion. Therefore a test with many assertions provides a limited amount of useful information as to the root cause of the failure.

The shouldGetPointsOnCircle() method already has too many assertions to be useful. Furthermore, to test it effectively you still need additional data points along the circle.

The way to resolve this issue is to refactor these test cases into multiple small cases which provide useful context during a failure. Each test will make a single assertion. Subsequently, if a single test fails, a single assertion has failed.

Walkthrough 3: Factoring into Multiple Test Methods

In this walkthrough you will perform the following tasks:

Steps

  1. Open the BasicCircleTest.as file from the previous exercise.

    Alternatively, if you didn't complete the previous lesson or your code is not functioning properly, you can import the FlexUnit4Training_wt3.fxp project from the Unit5/Start folder. Please refer to Unit 2: Walkthrough 1 for instructions on importing a Flash Builder project.

    Refactor point tests

  2. Replace the shouldGetPointsOnCircle() function with two new public functions in the BasicCircleTest class. These functions will be named so that each clearly represents the point on the circle being tested.

    [Test]
    public function shouldGetTopPointOnCircle():void {
    }
    
    [Test]
    public function shouldGetBottomPointOnCircle():void {
    }       

    Although the original shouldGetPointsOnCircle() method contained tests for only the top and bottom points of the Circle, in this Walkthrough you will add tests for the left and right points as well.

  3. Add two new methods named shouldGetRightPointOnCircle() and shouldGetLeftPointOnCircle().

  4. Copy the instantiation of the circle, and declaration of the point from the shouldgetPointsOnCircle() method into each of the four new methods.

    [Test]
    public function shouldGetTopPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point;
    }
    
    [Test]
    public function shouldGetBottomPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point;
    }
    
    [Test]
    public function shouldGetRightPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point;
    }
    
    [Test]
    public function shouldGetLeftPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point;
    }       
  5. Copy the block of code following //top-most point of circle comment into the shouldGetTopPointOnCircle() method. Copy the block following the //bottom-most point of circle comment into the shouldGetBottomPointOnCircle() method.

    [Test]
    public function shouldGetTopPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point;
    
        //top-most point of circle 
        point = circle.getPointOnCircle( 0 );
        assertEquals( 5, point.x );
        assertEquals( 0, point.y );
    }
    
    [Test]
    public function shouldGetBottomPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point;
    
        //bottom-most point of circle
        point = circle.getPointOnCircle( Math.PI );
        assertEquals( -5, point.x );
        assertEquals( 0, point.y );
    }       

    Because the methods each test specific points, the name is clear enough to distinguish one test from another. Remove the comment fields in each of the tests accordingly.

  6. Modify the shouldGetTopPointOnCircle() and shouldGetBottomPointOnCircle() methods to save space by instantiating the point variable as it is declared in each function, much like the circle variable. The comments can be removed as well.

    [Test]
    public function shouldGetTopPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point = circle.getPointOnCircle( 0 );
    
        assertEquals( 5, point.x );
        assertEquals( 0, point.y );
    }
    
    [Test]
    public function shouldGetBottomPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point = circle.getPointOnCircle( Math.PI );
    
        assertEquals( -5, point.x );
        assertEquals( 0, point.y );
    }       
  7. In the shouldGetRightPointOnCircle() method, set the point variable to circle.getPointOnCircle( Math.PI/2 ).

    [Test]
    public function shouldGetRightPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point = circle.getPointOnCircle( Math.PI/2 );
    }       
  8. In the shouldGetLeftPointOnCircle() method, set the point variable to circle.getPointOnCircle( (3*Math.PI)/2 ).

    [Test]
    public function shouldGetLeftPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point = circle.getPointOnCircle( (3*Math.PI)/2 );
    }       
  9. Add two calls to the assertEquals() method to shouldGetRightPointOnCircle(). One should assert that point.x is equal to 5, and point.y is equal to 0.

    [Test]
    public function shouldGetRightPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point = circle.getPointOnCircle( Math.PI/2 );
    
        assertEquals( 0, point.x );
        assertEquals( 5, point.y );
    }       
  10. Add two calls to the assertEquals() method to shouldGetLeftPointOnCircle(). One should assert that point.x is equal to 0, and point.y is equal to -5.

    [Test]
    public function shouldGetLeftPointOnCircle():void {
        var circle:Circle = new Circle( new Point( 0, 0 ), 5 );
        var point:Point = circle.getPointOnCircle( (3*Math.PI)/2 );
    
        assertEquals( 0, point.x );
        assertEquals( -5, point.y );
    }       
  11. Delete the previously used shouldGetPointsOnCircle() method, because its tests have been replicated.

  12. Save the BasicCircleTest.as file.

  13. Run the FlexUnit4Training.mxml file.

    If FlexUnit4Training.mxml ran successfully you should see the following output in your browser window:

    ThreeTestsFailed

    Figure 1: Three tests failed.

    Previously, when all the point assertions were bundled into a single method, it was impossible to determine which assertions caused the failure. Refactoring these assertions into four different test methods makes it clear that three of the four conditions are failures.

    Further inspection will reveal that the shouldGetTopPointOnCircle() is the only of the four tests that is passing. It is also the only test with an integer value of 0 passed in for its radians argument. The root cause of the situation is the way in which Flash Player handles fractions and the precision of those comparisons. This will be explored further and a solution demonstrated later in this text.

Summary

Navigation