Theories are a tool for testing a class against a potentially infinite set of data points. While theories are a powerful concept, they must work in conjunction with static tests to ensure proper functionality.
After completing this lesson, you should be able to:
In this unit, you will learn about the following topics:
When testing formulas, there are a potentially infinite number of values that may be given and return an expected result. It is impossible in these cases to test all possible values that could be passed to a test case.
Testing a single case is fairly weak, because any formula could return a hard-coded value. There is very little assurance in a single trial of anything. Likewise, a single test case is just slightly better than not testing at all.
If a test succeeds in two cases with two different values, already it has exponentially more assurance. If a test succeeds three or more times with three or more values, for each trial added it becomes substantially easier to trust the functionality being tested.
A small range of values is acceptable. Granted, it does not cover every case, but it does give you a substantial degree of assurance. A small range of values can also include all the types of values that could be problematic. For instance:
public function absoluteValue( value:int ):int {
if ( value < 0 ) {
return value * -1;
} else {
return value;
}
}
A simple method like the one presented above should be tested with at least five values: Positive integers, negative integers, 0, NaN, Infinity. That kind of small combination is fairly standard for arithmetic functions, but sometimes it becomes more complicated, particularly when float types are involved in the calculation.
While you may not be able to test every case, the more potential values that can be passed through the method to more assurance you have that the method does indeed function directly. We call this concept triangulation.
A FlexUnit theory is a method marked with the [Theory] metadata. Unlike the test methods you have worked with so far, theory methods can have parameters. Theory methods will be called multiple times with different data points, however, much like a mathematical theory; they are either valid or invalid, there is no sometimes.
Any one case that fails disproves the whole theory. Therefore any one time the theory method is called when it fails marks the entire test as a failure.
A simple theory method:
[Theory]
public function testTheory( value1:Number, value2:Number ):void
A test like the one in the previous section relies upon good data points to effectively triangulate. The tests you have used so far in this course have been statically coded and passed values to test for functionality. While that works for a small number of tests, it becomes increasingly more difficult to write and maintain as the number of tests and data points grow.
When attempting to triangulate a method, you need to be able to quickly create and add to a vast set of data points which will be provided to the test. In FlexUnit 4.x two special metadata tags named [DataPoint] and [DataPoints] can decorate data to indicate that it should be provided to the available tests as potential data points.
A data point is a single variable, or function that returns a value, which will be passed as an argument to a test for testing. A data point:
For Instance:
[DataPoint]
public static var value1:int = 10;
Data points are an array of variables, or a function that returns an array, used as arguments to a theory. Data points allow you to quickly specify many values that will be passed to a single theory.
[DataPoints]
metadata, must be decorated with [ArrayElementType("TYPE")]
specifying the type of the array elements.For instance:
[DataPoints]
[ArrayElementType("String")]
public static var stringValues:Array = ["one","two","three" ];
The ArrayElementType metadata describes the type of data in the Array to the theories in this class.
Theories are written to test a small function or calculation over a potentially large set of data. Using theories with a range of data, you build assurance that the theory is actually performing the calculation as opposed to just returning the expected result.
Some notes on theories:
[Theory]
metadata[DataPoint]
and [DataPoints]
metadata[RunWith("org.flexunit.experimental.theories.Theories")]
public class MyTheoryTest {
}
Theories run with all matching data points. If any combination of Datapoints
fail, the theory fails.
[DataPoints]
[ArrayElementType("Number")]
public static var radii:Array = [ 1, 2, 3, 4 ];
[Theory]
public function testTheory( value1:Number, value2:Number ):void
This theory takes two parameters. All Datapoints
referenced with the data type integer will be passed into each of these values for every possible combination.
Test process:
testTheory( 1, 1 )
testTheory( 1, 2 )
testTheory( 1, 3 )
...
testTheory( 4, 3 )
testTheory( 4, 4 )
Marking data points with the [ArrayElementType("Type")]
metadata ensures that only the desired input type is contained within the collection. Additionally, it makes sure that these values are passed in wherever a parameter of the specified type is used in a theory.
The following theory takes two number inputs and a string, but there is only a single collection of numbers and a single collection of strings within the class.
[DataPoints]
[ArrayElementType("Number")]
public static var numbers:Array = [ 1, 2, 3, 4, 5 ];
[DataPoints]
[ArrayElementType("String")]
public static var strings:Array = [ "Mike", "Tom", "Bob", "Cindy" ];
[Theory]
public function testNamesAndNumbers( name:String, numberOne:Number, numberTwo:Number ):void {
assertTrue( name.length > 0 );
assertTrue( numberOne > 0 );
assertTrue( numberTwo > 0 );
assertTrue( numberOne + numberTwo > 0 );
assertTrue( numberOne + numberTwo + name.length > 0 );
}
Numbers from the number array are used in the numberOne and numberTwo parameters, and the array of strings is used for the name parameter. The theory runs with all possible input combinations.
testNamesAndNumbers( "Mike", 1, 1 )
testNamesAndNumbers( "Mike", 1, 2 )
testNamesAndNumbers( "Mike", 1, 3 )
...
testNamesAndNumbers( "Cindy", 5, 4 )
testNamesAndNumbers( "Cindy", 5, 5 )
In this walkthrough you will perform the following tasks:
Select the math.testcases package from the previous exercise. Create a new class in the math.testcases package named CircleTheory.as. This class has no superclass or interfaces.
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 8/Start folder. Please refer to Unit 2: Walkthrough 1 for instructions on importing a Flash Builder project.
After the class has been created, the package directory should appear as follows:
Mark the new class with [RunWith("org.flexunit.experimental.theories.Theories")]
metadata, which should be placed just above the class definition.
[RunWith("org.flexunit.experimental.theories.Theories")]
public class CircleTheory {
...
}
Create a new public function named shouldShowAllRadiiEqual()
with a parameter named radius
of data type Number
. Mark the function with [Theory]
metadata:
[Theory]
public function shouldShowAllRadiiEqual( radius:Number ):void {
}
Add a variable named circle
of type Circle
to the shouldShowAllRadiiEqual()
method. Instantiate circle with an origin at (0, 0)
and the radius
parameter passed in as its radius.
[Theory]
public function shouldShowAllRadiiEqual( radius:Number ):void {
var circle:Circle = new Circle( new Point( 0, 0 ), radius );
}
If you did not use code-completion, add the imports for net.digitalprimates.math.Circle and flash.geom.Point at this time.
Add a call to the assertEquals()
method. Assert that the circle.radius
is equal to the radius
parameter.
[Theory]
public function shouldShowAllRadiiEqual( radius:Number ):void {
var circle:Circle = new Circle( new Point( 0, 0 ), radius );
assertEquals( radius, circle.radius );
}
If you did not use code-completion, add an import for org.flexunit.asserts.assertEquals at this time.
Add a public static array to the class. Fill it with a variety of positive integer values. Here is an example:
public static var radii:Array = [ 1,2,3,4,5,6,7,8,9,10 ];
Mark the array with two lines of metadata, [DataPoints]
and [ArrayElementType("Number")]
.
[DataPoints]
[ArrayElementType("Number")]
public static var radii:Array = [ 1,2,3,4,5,6,7,8,9,10 ];
Save CircleTheory.as.
Open the CircleSuite.as file within the math.testcases package. Add a new public variable named test3
with a type of CircleTheory
.
[Suite]
[RunWith("org.flexunit.runners.Suite")]
public class CircleSuite {
public var test1:BasicCircleTest;
public var test2:CircleConstructorTest;
public var test3:CircleTheory;
}
If you did not use code-completion, add the import for math.testcases.CircleTheory at this time.
Save the CircleSuite.as file.
Run the FlexUnit4Training.mxml file.
If FlexUnit4Training.mxml ran successfully you should see the following output in your browser window:
Assumptions are used in conjunction with theories to limit acceptable data points. Assumptions allow theories to setup basic constraints and limitations for the methods and formulae being tested.
Some notes on assumptions:
Datapoint
sets pass the assumptions the test is marked as a failureassumeThat( value, greaterThan(0) );
If the parameter is greater than 0, the assumption passes and the test moves onto its next line. If the parameter value is not greater than 0 the assumption fails and the runner will move on to the next data point without running any other lines of this test. Even though the assumption fails for values under 0, the test does not fail or throw an error, because those data points have been essentially marked as invalid for the test theory.
In this walkthrough you will perform the following tasks:
Open the CircleTheory.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 Unit8/Start folder. Please refer to Unit 2: Walkthrough 1 for instructions on importing a Flash Builder project.
Add a single negative value to the beginning of the class's radii array. Here is an example:
public static var radii:Array = [ -5,1,2,3,4,5,6,7,8,9,10 ];
Run the FlexUnit4Training.mxml file.
If FlexUnit4Training.mxml ran successfully you should see the following output in your browser window:
The theory failed because -5 is an invalid radius for a Circle object. If one of the theories assertions fails, the entire theory fails.
Add a new line to the shouldShowAllRadiiEqual()
method. On the line, add an assumption indicating that this test only works for positive radii. The method should read as follows:
[Theory]
public function shouldShowAllRadiiEqual( radius:Number ):void {
assumeThat( radius, greaterThan( 0 ) );
var circle:Circle = new Circle( new Point( 0, 0 ), radius );
assertEquals( radius, circle.radius );
}
If you did not use code-completion, add the import statements for org.flexunit.assumeThat and org.hamcrest.number.greaterThan at this time.
Save the CircleTheory.as file.
Run the FlexUnit4Training.mxml file.
If FlexUnit4Training.mxml ran successfully you should see the following output in your browser window:
The assumeThat( radius, greaterThan( 0 ) )
statement assures that the theory is only testing radii that are valid, or in this case, positive. The theory ignores the -5 input and all negative inputs thanks to this statement.
FlexUnit 4 theories include support for complex objects as data points. Because ActionScript uses so many complex objects, it's not uncommon to have them passed as arguments in tests and theories. Theories are much more useful given the ability to deal with complex data points.
Some notes on complex data points:
ArrayElementType
in the DataPoints
ArrayElementType
requires the full class path for the type, such as flash.geom.Point
or mx.collections.ArrayCollection
.For Instance:
[DataPoints]
[ArrayElementType("flash.geom.Point")]
Public static var points:Array = [ new Point( 0, 0 ) ];
Theory case constructors can be used like the constructors of many classes. In test cases and theories, constructors can be passed initial data for use by all or some of the methods within the class. For instance:
public class TestCase {
[DataPoints]
[ArrayElementType("flash.geom.Point")]
public static var points:Array = [ new Point( 0, 0 ),
new Point( 10, 10 ),
new Point( -5, 5 ) ];
[DataPoints]
[ArrayElementType("Number")]
public static var radii:Array = [ 0, 5, 10 ];
public static var circle:Circle;
public function TestCase( origin:Point, radius:Number ):void {
circle = new Circle( origin, radius );
}
...
}
This method can help to reduce the complexity of the created complex objects, or it can serve to convert existing sets of data points into use within other complex objects.
If the complex objects are passed to the class constructor, they no longer need to be passed in as arguments to the theories. The constructor will be run before each test or theory in the case. Each test method can then use the newly instantiated class variables, which will be re-instantiated before each test is run.
In this way, the class tends to be more cohesive and about a specific set of data rather than a free for all of unrelated theories working on data points.
In this walkthrough you will perform the following tasks:
Open the CircleTheory.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 Unit8/Start folder. Please refer to Unit 2: Walkthrough 1 for instructions on importing a Flash Builder project.
Add a private static constant named TOLERANCE
of data type Number
to the CircleTheory class.
private static const TOLERANCE:Number = .0001;
Add a new method named shouldShowAllPointsEqual()
to the class.
[Theory]
public function shouldShowAllPointsEqual(origin:Point):void {
}
Create a new circle with the function's origin argument and a radius of 10.
[Theory]
public function shouldShowAllPointsEqual(origin:Point):void {
var circle:Circle = new Circle(origin, 10);
}
Call the circle.getPointOnCircle()
method with argument Math.PI
.
[Theory]
public function shouldShowAllPointsEqual( origin:Point ):void {
var circle:Circle = new Circle( origin, 10 );
var pointOnCircle:Point = circle.getPointOnCircle( Math.PI );
}
Declare a variable named distance
of data type Number
within the shouldShowAllPointsEqual()
method. Instantiate it to Point.distance( origin, pointOnCircle )
.
[Theory]
public function shouldShowAllPointsEqual( origin:Point ):void {
var circle:Circle = new Circle( origin, 10 );
var pointOnCircle:Point = circle.getPointOnCircle( Math.PI );
var distance:Number = Point.distance( origin, pointOnCircle );
}
Add a call to the assertThat()
method. It should assert that distance
variable is closeTo( circle.radius, TOLERANCE )
.
[Theory]
public function shouldShowAllPointsEqual( origin:Point ):void {
var circle:Circle = new Circle( origin, 10 );
var pointOnCircle:Point = circle.getPointOnCircle( Math.PI );
var distance:Number = Point.distance( origin, pointOnCircle );
assertThat( distance, closeTo( circle.radius, TOLERANCE ) );
}
If you did not use code-completion, add the imports for org.flexunit.assertThat and org.hamcrest.number.closeTo at this time.
Add a new public static array named points
to the class. Initialize the array with six point values, representing a gamut of potential points.
public static var points:Array = [ new Point( 0, 0 ),
new Point( 10, 10 ),
new Point( -5, 5 ),
new Point( 20, -20 ),
new Point( -17, -16 ),
new Point( 5.2, -11.3 ) ];
Mark the array with [DataPoints]
and [ArrayElementType("flash.geom.Point")]
metadata. Place these tags on the two lines above the array:
[DataPoints]
[ArrayElementType("flash.geom.Point")]
public static var points:Array = [ new Point( 0, 0 ),
new Point( 10, 10 ),
new Point( -5, 5 ),
new Point( 20, -20 ),
new Point( -17, -16 ),
new Point( 5.2, -11.3 ) ];
Save CircleTheory.as
Run the FlexUnit4Training.mxml file.
If FlexUnit4Training.mxml ran successfully you should see the following output in your browser window:
Alter the shouldShowAllPointsEqual()
method to accept a second parameter named radius
of data type Number
. The radius
parameter will be passed to the circle constructor. You will need to add an assumeThat( radius, greaterThan(0) );
statement to the first line of the shouldShowAllPointsEqual()
method.
[Theory]
public function shouldShowAllPointsEqual( origin:Point, radius:Number ):void {
assumeThat( radius, greaterThan( 0 ) );
var circle:Circle = new Circle( origin, radius );
var pointOnCircle:Point = circle.getPointOnCircle( Math.PI );
var distance:Number = Point.distance( origin, pointOnCircle );
assertThat( distance, closeTo( circle.radius, TOLERANCE ) );
}
Save CircleTheory.as
Run the FlexUnit4Training.mxml file.
If FlexUnit4Training.mxml ran successfully you should see the following output in your browser window:
Because the shouldShowAllRadiiEqual()
theory passes by constructing valid Circle objects with radius parameters from the radii array, it should be no surprise that the shouldShowAllPointsEqual()
method passes using those data points.
Valid data points should be consistent for all theories.
Re-Open to the CircleTheory.as file.
Add a parameter to the shouldShowAllPointsEqual()
method named radians
of data type Number
. Alter the circle.getPointOnCircle()
method so that it takes radians
as its arguments.
[Theory]
public function shouldShowAllPointsEqual( origin:Point, radius:Number, radians:Number ):void {
assumeThat( radius, greaterThan( 0 ) );
var circle:Circle = new Circle( origin, radius );
var pointOnCircle:Point = circle.getPointOnCircle( radians );
var distance:Number = Point.distance( origin, pointOnCircle );
assertThat( distance, closeTo( circle.radius, TOLERANCE ) );
}
Save CircleTheory.as
Run the FlexUnit4Training.mxml file.
If FlexUnit4Training.mxml ran successfully you should see the following output in your browser window:
The circle.getPointOnCircle()
method should return a valid point on the circle regardless of the value of the radians field. Any number within the radii array should be a valid radians argument, and therefore the theory passes with these data points.
Data points and theories allow many values to be tested without a great deal of complexity.
Metadata:
Theory classes are marked with [RunWith("org.flexunit.experimental.theories.Theories")]
metadata.
Single data points are marked with [DataPoint]
metadata.
Arrays of data points are marked with [DataPoints]
and [ArrayElementType("TYPE")]
metadata.
Theories are marked with [Theory]
metadata.
Theories can create a test fixture using the class constructor.
Complex objects can be passed as data points.