Theories, Datapoints, and Assumptions
From FlexUnit4 Project Documentation
Contents |
[edit] Assumptions
Before beginning this section I will assume that you understand testCases, metadata, a small amount of Hamcrest and assertions.
[edit] Why use Theories
When writing unit tests the process can often become tedious when trying to test a wide range of values, be it integers, dates or user created objects. It is not terribly uncommon to have the need for tests that are extremely similar with only different values passed in. Theories will allow you to specify a type-specific set of datapoints thus having FlexUnit 4 provide you with all possible combinations and said outcomes.
[edit] What is a Theory?
Theories are tests written against set(s) of data. As such, the test itself must be generic enough to accept these variable sets of data. Theories many times inherit the concept of testing a reversible mathematical formula and because of this we first must test against some human checked answers before moving onto an infinite set of possibilities. The reason is, we need to verify that the theory is actually performing some calculation and not just spitting out the expected result. So how do we do this? Well let's take a look at Example #1 and see what it's composed of.
Lines 2 we declare our import on the type of assertion we will make later in the Theory.
import org.flexunit.asserts.assertEquals;
Line 3 goes in conjunction with line 5, importing the runner we will be using to run the tests.
import org.flexunit.experimental.theories.Theories;
Line 5 is telling Flexunit to use a specific runner, in this case a Theory runner. If you're curious, here's more on RunWith
[RunWith("org.flexunit.experimental.theories.Theories")]
Line 7 is necessary only if this is the first time that the runner is being specified.
private var theory:Theories;
On lines 9 & 12 we have a new metadata called DataPoint or DataPoints. Datapoints are a single variable or an array of values that are used with tests written inside a theory to verify an expected result. A Datapoint is declared as a static variable of any basic type with the metadata tag [DataPoint] before it. Similarly, you can have Datapoints that are an array containing n number of values of any basic type that is declared with a [DataPoints] tag before the variable, more on DataPoints in a later example.
[DataPoint] public static var value1:int = 10;
On line 14 we have another new metadata tag [Theory].
[Theory]
On line 15, we have our function being defined with parameters. Functions and DataPoints are going to give us the ability to test our Theories against a wide range of combinations that FlexUnit will handle for us.
public function testDivideMultiply( value1:int, value2:int ):void
Lines 17-21 should look similar, much like all those test functions you've been writing. We're going to save off the quotient of value1 and value2, then take the product of our quotient and value2 and verify that in the end our product and value1 are equal.
Example #1
[1] package flexUnitTests.theorySuite { [2] import org.flexunit.asserts.assertEquals; [3] import org.flexunit.experimental.theories.Theories; [4] [5] [RunWith("org.flexunit.experimental.theories.Theories")] [6] public class TheorySuite { [7] private var theory:Theories; [8] [9] [DataPoint] [10] public static var value1:int = 10; [11] [DataPoint] [12] public static var value2:int = 5; [13] [14] [Theory] [15] public function testDivideMultiply( value1:int, value2:int ):void { [16] [17] var div:Number = simpleMath.divide( value1, value2 ); [18] [19] var mul:Number = simpleMath.multiply( div, value2 ); [20] [21] Assert.assertEquals( mul, value1 ); [22] } [23] } [24] }
At first glance you might assume that 10 gets passed in as value1, and 5 in as value2, the calculation gets performed and the theory succeeds and proceeds on to the next one. That is not the case however.
When datapoints are created, FlexUnit is going to check the Theory to see how many parameters it takes and then pass in all the possible combinations the datapoints make up. This gets even more powerful when we are able to declare type-specific arrays, again more on this later. So what you would actually see is 4 passthroughs for this one theory, using all possible combinations of 5 and 10.
When combining tests with theories you're able to discover values that violate your tests much more easily than with normal tests, and since this theory is a mathematical formula the above combinations aren't sufficient enough to proclaim this theory works. We need to test against a large range of data, and FlexUnit 4 gives us that ability by the use of these type-specific arrays that I've been holding off for till now. So let's add another test to our example as well as some more DataPoints.
Lines 4-6 we have a couple more imports
import org.flexunit.assumeThat; import org.hamcrest.number.greaterThan; import org.hamcrest.object.instanceOf;
On Lines 19 & 23, what you've probably been waiting for, we've added two DataPoints. When declaring DataPoints, a type-specific array, we need to tell FlexUnit what the type is. We do this by adding the metadata with [ArrayElementType("type")]. You can manually create the arrays yourself or you can load in data from an external source. This latter part won't be covered in this section, more info on using external data here.
[DataPoints]
[ArrayElementType("Number")]
public static var numberValues:Array = [-5,-3.5,-1,0,1,2.2,3,4,5,6,7,8,9];
[DataPoints]
[ArrayElementType("String")]
public static var stringValues:Array = ["one","two","three","four","five"];
Line 27 is still the same, 28 has a small change to the type of parameters now being accepted into the Theory.
Line 30 is introducing the concept of assumptions. Assumptions are the tests written within the theory expecting certain behavior for the data passed to it. Keep in mind that these assumptions will be run against all DataPoints that match the assumptions argument types, so the tests must be written in such a way to fulfill all possible combinations of data. If the assumption isn't able to proceed with any of the values passed to it, the test will fail. In the below example the function will be passed all combinations of Number datapoints (i.e.. -5,-3.5,-1,0,1,2.2,3,4,5,6,7,8,9) but not value1, value2 or stringValues. The assumption though is only going to allow passthroughs of the test when value2 is greater than 0, if the combination of numbers has value2 less than or equal to 0, the theory will move on to the next combination.
assumeThat(value2, greaterThan(0));
The following Theory on line 40 shows a basic test which accepts String values and, using a hamcrest test, asserts that the values passed in are of type String. Even though we have numberValues, ints and stringValues declared as DataPoints the test should only verify the String values.
public function testStringOnly( value1:String ):void {
assertThat( value1, instanceOf(String) );
}
This concludes what a Theory is. The next topic, theory constructors, the use of them is a personal preference just giving you another way of injecting information into tests and is completely optional.
Example #2
[1] package flexUnitTests.theorySuite { [2] [3] import org.flexunit.asserts.assertEquals; [4] import org.flexunit.assumeThat; [5] import org.hamcrest.number.greaterThan; [6] import org.hamcrest.object.instanceOf; [7] [8] import org.flexunit.experimental.theories.Theories; [9] [10] [RunWith("org.flexunit.experimental.theories.Theories")] [11] public class TheorySuite { [12] private var theory:Theories; [13] [DataPoint] [14] public static var value1:int = 10; [15] [16] [DataPoint] [17] public static var value2:int = 5; [18] [19] [DataPoints] [20] [ArrayElementType("Number")] [21] public static var numberValues:Array = [-5,-3.5,-1,0,1,2.2,3,4,5,6,7,8,9]; [22] [23] [DataPoints] [24] [ArrayElementType("String")] [25] public static var stringValues:Array = ["one","two","three","four","five"]; [26] [27] [Theory] [28] public function testDivideMultiply( value1:Number, value2:Number):void { [29] [30] assumeThat(value2, greaterThan(0)); [31] [32] var div:Number = simpleMath.divide( value1, value2 ); [33] [34] var mul:Number = simpleMath.multiply( div, value2 ); [35] [36] Assert.assertEquals( mul, value1 ); [37] [38] } [39] [40] [Theory] [41] public function testStringOnly( value1:String ):void { [42] assertThat( value1, instanceOf(String) ); [43] } [44] } [45] }
[edit] Theory Class Constructors
Before beginning this section, I will be assuming that you have a clear understanding on Theories and how they work.
The concept behind using Theory class constructors is to break up the understanding of what is actually being tested inside a Theory.
The example below is just like any Theory class. We have DataPoints of type Circle, and another of type Number that holds a bunch of angles.
The Theory will read in all possible combinations of circles and angles, and test the function getPointOnCir()
This works perfectly fine, however, if you wanted to refactor the circle out of the parameters so as not to indicate that the circle itself is being tested, you can rewrite the same functionality as in Example #2.
Example #1
[RunWith("org.flexunit.experimental.theories.Theories")] public class TheorySuite { private var theory:Theories; [DataPoints] [ArrayElementType("myShapes.Circle")] public static var circles:Array = [new Circle(new Point(1,-1),10), new Circle(new Point(10,-10),40), new Circle(new Point(-5,3),11), new Circle(new Point(4,4),6)]; [DataPoints] [ArrayElementType("Number")] public static var angles:Array = [0,10,20,5,33,44.5,60,90,180]; [Theory] public function testGetPointOnCir(circle:Circle, angle:Number):void { var centerXY:Point = circle.center; var radius:Number = circle.radius; var newPoint:Point = circle.getPointOnCir(angle); var testCenter:Point = calcCenter(newPoint, radius); assertEquals(testCenter,centerXY); } }
The only difference is that now the constructor of our Theory class is going to save each of type circle, save it to the local variable and run the theory for each angle for each circle. Same functionality as before, just handled a bit differently.
Example #2
[RunWith("org.flexunit.experimental.theories.Theories")] public class TheorySuite { private var theory:Theories; [DataPoints] [ArrayElementType("myShapes.Circle")] public static var circles:Array = [new Circle(new Point(1,-1),10), new Circle(new Point(10,-10),40), new Circle(new Point(-5,3),11), new Circle(new Point(4,4),6)]; [DataPoints] [ArrayElementType("Number")] public static var angles:Array = [0,10,20,5,33,44.5,60,90,180]; public static var circle:Circle; [Theory] public function testGetPointOnCir(angle:Number):void { var centerXY:Point = circle.center; var radius:Number = circle.radius; var newPoint:Point = circle.getPointOnCir(angle); var testCenter:Point = calcCenter(newPoint, radius); assertEquals(testCenter,centerXY); } public function TheorySuite(circleConstruc:Circle):void { circle = circleConstruc; } }
