Intro To The VFPUnit Workbench. Part 4 I passed over 'packaging' earlier, and will continue to avoid the 'why'. First, I'd like to show you some of the end variations of the 'how'. It is a demo of the end results achieved after 'code shuffling' a few different ways. It suggests a general progression where by, classes are assembled from functionality, methods are shifted amongst classes, classes 'percolate' to the appropriate level, and in general, business objects are 'grown' from functional requirements. | |||
'Packaging' Variations 1) Functionality is found in public namespace. i.e. the 'cast to the wind and hope it's there' call. We just did that one. We can view this approach as Foxpro owning the functionality, and the names appear to be (undocumented) intrinsic functions. Loading of the functionality is done with the use of a 'set proc' statement, or by generating an app or exe. | |||
2) Similar to 1), but instead of automatically loading the source file as a procedure file, we explicitly load the procedure file in the 'setup' of the fixture, and release it in the 'teardown'. This lets you create a new source file against a backdrop of preloaded functionality. | |||
Copy the existing test code. Create a new experiment Call it 'D' Paste in the test code Add 'set proc and release proc' statements to fixture Run Give the fixture a name and save it. | |||
3) Functions in the Test Code This gets a little more interesting. We can place the functions from the source file, directly into the test code (at the end of the code). You might think this strange, and it is. but it is useful as an intermediate. A little background: the test code text is used as part of a class definition created when run. Specifically, it is used as the code for a 'MyRun' method. So, we are really tricking the underlying text generating mechanism to include these functions as method calls inside the class definition. This has 2 consequences. a) the functions (to be methods) must be at the end of the test code text. b) Since they are methods, all calls need to use the reference: 'this.' Both in the test code, and any inter-function calls. (e.g. if InterestCalc() made a to call BizMonths() ) These are easy to accomplish. I'd suggest using find and replace on the function calls to convert them to method calls first, and then paste in a copy of the functions. | |||
*- Interest amount is rate * account amount * duration (in months)
TEST( 5.25 = this.InterestCalc( 5.25, 1000, 12), 'Interest not rate * amount * duration' +KCR +;
"Expect 5.25, Returned " +STR( this.InterestCalc( 5.25, 1000, 12), 10, 2) )
TEST(10.50 = this.InterestCalc( 5.25, 2000, 12), 'Interest not rate * amount * duration')
TEST( 0 = this.InterestCalc( 5.25, 0, 12), 'Interest not zero for zero accounts')
TEST( 0 = this.InterestCalc(-5.25, 1000, 12), 'Interest not zero for negative rate')
TEST( 0 = this.InterestCalc( 5.25,-1000, 12), 'Interest not zero for negative account')
TEST( 0 = this.InterestCalc( 5.25, 1000, -12), 'Interest not zero for negative duration')
TEST( 0 = this.InterestCalc(-5.25,-1000, 12), 'Interest not zero for negative rate, account')
TEST( 0 = this.InterestCalc(-5.25, 1000, -12), 'Interest not zero for negative rate, duration')
TEST( 0 = this.InterestCalc(-5.25,-1000, -12), 'Interest not zero for negative rate, account, duration')
TEST( 0 = this.InterestCalc( 5.25, 1000, 0), 'Interest not zero for new accounts')
TEST( 5.25 = this.InterestCalc( 5.25, 1000, this.BizMonths({1/1/1999}, {1/1/2000}) ),'Incorrect Month Calculation' )
TEST(12 = this.BizMonths({1/1/1999}, {1/1/2000}), '1 calendar year not seen as 12 months')
TEST( 0 = this.BizMonths({1/1/1999}, {1/1/1999}), 'same day seen as a month or more')
TEST( 0 = this.BizMonths({1/15/1999}, {2/15/1999}), 'must calculate complete calendar months.')
TEST( 0 = this.BizMonths({1/1/1999}, {1/15/1999}), 'partial month seen as a month')
TEST( 1 = this.BizMonths({1/1/1999}, {2/15/1999}), 'partial month rounded up')
TEST(13 = this.BizMonths({1/1/1999}, {2/15/2000}), 'partial month rounded up')
TEST( 0 = this.BizMonths({2/1/2000}, {2/29/2000}), 'leap year February rounded up')
TEST( 0 = this.BizMonths({1/1/2000}, {1/1/1999}), 'Invalid format [end before start] seen as valid')
********************
FUNCTION InterestCalc
PARAMETERS tnRate, tnAmount, tnMonths
IF tnRate > 0 .and. ;
tnAmount > 0 .and. ;
tnMonths > 0
RETURN (tnRate/(100 *10) ) * tnAmount * (tnMonths/12)
ELSE
RETURN 0
ENDIF
********************
FUNCTION BizMonths
PARAMETERS tdStart, tdEnd
*- begin at the first of the month, and count backward
*- until the first of that month is less than the start date.
ldBegin = CTOD( ALLTRIM(STR(MONTH(tdEnd))) +'/' +'1/' +ALLTRIM(STR(YEAR(tdEnd))) )
lnMonths = 0
DO WHILE .T.
ldFirstOfMonth = GOMONTH( ldBegin, -1)
IF tdStart > ldFirstOfMonth
EXIT
ELSE
ldBegin = ldFirstOfMonth
lnMonths = lnMonths +1
ENDIF
ENDDO
RETURN lnMonths
| |||
|
| |||
4) By the same token, The functions could be incorporated as methods of the fixture. In this case, the methods need to be referenced with the built-in keyword 'Fixture' The functional code can be placed in any of the 3 fixture code editboxes. | |||
TEST( 5.25 = Fixture.InterestCalc( 5.25, 1000, 12), 'Interest not rate * amount * duration' +KCR +;
"Expect 5.25, Returned " +STR( Fixture.InterestCalc( 5.25, 1000, 12), 10, 2) )
TEST(10.50 = Fixture.InterestCalc( 5.25, 2000, 12), 'Interest not rate * amount * duration')
TEST( 0 = Fixture.InterestCalc( 5.25, 0, 12), 'Interest not zero for zero accounts')
TEST( 0 = Fixture.InterestCalc(-5.25, 1000, 12), 'Interest not zero for negative rate')
and so on ..,
| |||
|
| |||
5) We can wrap the functions into a class defined in the test code. We must then create an object from that class, and convert the test calls to reference that object. | |||
LOCAL oCalcs
oCalcs = CREATEOBJECT("BagoFuncs")
TEST( 5.25 = oCalcs.InterestCalc( 5.25, 1000, 12), 'Interest not rate * amount * duration' +KCR +;
"Expect 5.25, Returned " +STR( oCalcs.InterestCalc( 5.25, 1000, 12), 10, 2) )
TEST(10.50 = oCalcs.InterestCalc( 5.25, 2000, 12), 'Interest not rate * amount * duration')
TEST( 0 = oCalcs.InterestCalc( 5.25, 0, 12), 'Interest not zero for zero accounts')
TEST( 0 = oCalcs.InterestCalc(-5.25, 1000, 12), 'Interest not zero for negative rate')
TEST( 0 = oCalcs.InterestCalc( 5.25,-1000, 12), 'Interest not zero for negative account')
TEST( 0 = oCalcs.InterestCalc( 5.25, 1000, -12), 'Interest not zero for negative duration')
TEST( 0 = oCalcs.InterestCalc(-5.25,-1000, 12), 'Interest not zero for negative rate, account')
TEST( 0 = oCalcs.InterestCalc(-5.25, 1000, -12), 'Interest not zero for negative rate, duration')
TEST( 0 = oCalcs.InterestCalc(-5.25,-1000, -12), 'Interest not zero for negative rate, account, duration')
TEST( 0 = oCalcs.InterestCalc( 5.25, 1000, 0), 'Interest not zero for new accounts')
TEST( 5.25 = oCalcs.InterestCalc( 5.25, 1000, oCalcs.BizMonths({1/1/1999}, {1/1/2000}) ),'Incorrect Month Calculation' )
TEST(12 = oCalcs.BizMonths({1/1/1999}, {1/1/2000}), '1 calendar year not seen as 12 months')
TEST( 0 = oCalcs.BizMonths({1/1/1999}, {1/1/1999}), 'same day seen as a month or more')
TEST( 0 = oCalcs.BizMonths({1/15/1999}, {2/15/1999}), 'must calculate complete calendar months.')
TEST( 0 = oCalcs.BizMonths({1/1/1999}, {1/15/1999}), 'partial month seen as a month')
TEST( 1 = oCalcs.BizMonths({1/1/1999}, {2/15/1999}), 'partial month rounded up')
TEST(13 = oCalcs.BizMonths({1/1/1999}, {2/15/2000}), 'partial month rounded up')
TEST( 0 = oCalcs.BizMonths({2/1/2000}, {2/29/2000}), 'leap year February rounded up')
TEST( 0 = oCalcs.BizMonths({1/1/2000}, {1/1/1999}), 'Invalid format [end before start] seen as valid')
****************************************
DEFINE CLASS BagoFuncs AS Custom
********************
FUNCTION InterestCalc
PARAMETERS tnRate, tnAmount, tnMonths
IF tnRate > 0 .and. ;
tnAmount > 0 .and. ;
tnMonths > 0
RETURN (tnRate/(100 *10) ) * tnAmount * (tnMonths/12)
ELSE
RETURN 0
ENDIF
********************
FUNCTION BizMonths
PARAMETERS tdStart, tdEnd
*- begin at the first of the month, and count backward
*- until the first of that month is less than the start date.
ldBegin = CTOD( ALLTRIM(STR(MONTH(tdEnd))) +'/' +'1/' +ALLTRIM(STR(YEAR(tdEnd))) )
lnMonths = 0
DO WHILE .T.
ldFirstOfMonth = GOMONTH( ldBegin, -1)
IF tdStart > ldFirstOfMonth
EXIT
ELSE
ldBegin = ldFirstOfMonth
lnMonths = lnMonths +1
ENDIF
ENDDO
RETURN lnMonths
ENDDEFINE
| |||
|
| |||
6) And yes, We can wrap the functions into a class defined in the Fixture code. Classes defined in a fixture must be placed at the end of the 'Teardown' code. We must then : create an object from that class in the setup of the fixture. store that reference to a property of the fixture and convert the test calls to reference the object stored in the fixture property. (it's easier than it sounds). | |||
and lastly, we could have placed the class definition in the procedure file in the first place. | |||
All of these variants are useful. Here are a few general observations: Functions in the test code are especially useful for operations that are common within that experiment. Often they suggest missing functionality in the source class. Classes in test code are often useful for quick 'wrappers' or adapters on existing classes. Often, they suggest or become missing classes. You can also define a class to be a subclass of a class defined in the source file. That way, you can test new functionality, without touching the original source code. And finally, Simply put, Fixtures tend to become business objects. Functions suggest methods on that bizobj, and classes suggest that the bizobj is a collection/composite. Even though I've stressed .prgs as source files, classes defined in class libraries (.vcxs) work equally well. Analogously, this program will 'set classlibrary to' the source vcx at 'run' time, to provide access to the class definitions. The class entered on the experiment form will be opened from the source vcx in the class designer when you press the 'Modify' button next to the source file. | |||
If we look at the similarities between the variants, other than the first case with naked functions, the only thing that really changes (other than the location of the code), is the variable referencing the object. By assigning the reference to a local variable first, and then using the local variable in the test expressions, you can easily change from one variant to another. Note how that local variable would be assigned this in variation #3), making it very similar to the other variations. i.e. LOCAL oCalcs oCalcs = this - would transform tests in #3) into tests of #5), making the test code 'portable' wrt the location of the functionality. | |||
| Next | Previous | Main |
Generated 03/11/02 01:49:41 PM |