While developing the version 2.0 of the
LifeCycle Build utility, it became evident that I was going to need a means to
test it as I added new feature and refractor the old code. I knew of the open
source utility called FoxUnit available at http://www.foxunit.org/
. But I was looking for something more inline like how NUnit is applied. Where
Test Code is actually part of the production code. The tests as embedded, at
times, with the actually production code. This appeared to make better sense
then have the test cases external to the code that is being tested. If the
code and test cases are together, then if I reuse the routine in other
projects, I will not have to move or copy the test cases. Also, with the test
case as apart of the source code, I can scan the source for the test cases,
then extract, and execute them. After some consideration as to how this would
all work I came up with FUnit.
FUnit (the “F” stands for FoxPro) is an Open
Source / Public Domain tool. It is a class that becomes apart of the Debug
version of production projects. After coding some or all of the test cases, we
simply call the FUnit’s Run method, passing it an object reference to be
tested. It scans the source code of the object and executes the test cases.
This in turn produces a test result log. The log contains the results of each
of the test cases and any error caught by the tests. In addition to just
running the test cases, FUnit will also perform Coverage analysis with code output.
Figure 1
illustrates a sample code with a class definition and the FUnit test blocks
(highlighted in blue). At the head of the example, we first include the
FUint.h header file. This provides us with the preprocessors we will need to
define the test cases. Next, we instantiate our object to the Accounting object.
We will use this object as reference when we start our test run. The FUnit
object is created by calling the FUnit program. It creates a public variable
called _FUnit. (The underscore is added to make resemble other FoxPro system
variables.) Then we call the Run method of the _FUnit object; passing it the
object reference of the Accounting (oApp) object.
FUnit will scan the object heritage,
looking in to the source of the methods for Test Cases. When one is found, it
is extracted and executed. The tests results are determined with the help of
built in test case asserts. The asserts are accessed via the _FUnit global
object. (See the INIT event in the object defined in Figure 1. This code is from the program file
called Example.prg.)
|
*
Program: EXAMPLE.PRG
*
Description: Example code using the FUnit unit test class.
*
Created: 11/16/2006
*
Developer: Gregory L Reichert - UT #046387
*
Copyright: Copyright (c) 2006 GLR software
*------------------------------------------------------------
*--------------------------------------------------
* Include the FUnit header file
*--------------------------------------------------
#include INCLUDE\FUnit.h
*--------------------------------------------------
* Create
and gain access to the Accounting object
*--------------------------------------------------
PRIVATE oApp
oApp =
CREATEOBJECT("Accounting")
*--------------------------------------------------
* RUN the tests
*--------------------------------------------------
DO prg\FUnit
_funit.maint.Coverage.enabled = .T.
_funit.maint.Coverage.IncludeCode = .t.
? _FUnit.RUN( oApp )
*--------------------------------------------------
* release
the objects
*--------------------------------------------------
RELEASE oApp
RELEASE _FUnit
RETURN
***************************************************
*--------------------------------------------------
* Class
Definition.
*--------------------------------------------------
DEFINE
CLASS Accounting AS SESSION
nBalance = 0
PROCEDURE INIT
nInitBal = 0.00
this.nBalance = m.nInitBal
#IF COMPILE_TESTCASES
TEST_SUITE Validate properties
TEST_CASE (41952554) Validate
nBalance property
*-- setup
*-- do test
oApp.Init()
_FUnit.isEqual( 0, oApp.nBalance, "nBalance is
not initially zero.")
*-- cleanup
ENDTEST_CASE
#ENDIF
ENDPROC
FUNCTION Balance()
RETURN THIS.nBalance
#IF COMPILE_TESTCASES
TEST_SUITE Balance method
TEST_CASE (42158287) Ensure the
Balance method returns the proper value.
*-- setup
LOCAL lnBalance
lnBalance = oApp.nBalance
oApp.nBalance = 10.12
*-- do test
_FUnit.isNotEqual( 0, oApp.Balance(), "The
nBalance property did not change after being set.")
_FUnit.isEqual( 10.12, oApp.Balance(), "The
nBalance property get set properly." )
*-- cleanup
oApp.nBalance = lnBalance
ENDTEST_CASE
#ENDIF
ENDFUNC
FUNCTION Deposit
LPARAMETERS tnAmount AS NUMBER
THIS.nBalance = THIS.nBalance + tnAmount
RETURN this.Balance()
#IF COMPILE_TESTCASES
TEST_SUITE Deposit method
TEST_CASE (42715240) Positive
amount
*-- setup
LOCAL lnBalance
lnBalance = oApp.nBalance
oApp.nBalance = 100.00 && reset to
stable state
*-- do test
LOCAL lnReturn
lnReturn = oApp.Deposit( 100.00 )
_FUnit.isEqual( 200.00, oApp.nBalance, "The
deposit was not accumulated correctly." )
_FUnit.isTrue(
TYPE("lnReturn")=="N", "A non-numeric data type was
returned.")
_FUnit.isEqual( 200.00, oApp.nBalance )
*-- cleanup
oApp.nBalance = lnBalance
ENDTEST_CASE
TEST_CASE (43001537) Negitive
amount
*------------------------------------------------------------
* In a real world, deposits are Always positive.
* The negitive amount passed should be rejected.
*------------------------------------------------------------
*-- setup
LOCAL lnBalance
lnBalance = oApp.nBalance
oApp.nBalance = 100.00 && reset to
stable state
*-- do test
oApp.Deposit( -50.00 )
_FUnit.isNotEqual( 50.00, oApp.nBalance,
"The amount was add as a negative to the balance." )
_FUnit.isEqual( 100.00, oApp.nBalance,"The
amount was not add to the balance, but the balance is still off." )
*-- cleanup
oApp.nBalance = lnBalance
ENDTEST_CASE
#ENDIF
ENDFUNC
FUNCTION Withdrawl( tnAmount as Number ) as Number
IF tnAmount <= this.Balance()
this.nBalance = this.nBalance - tnAmount
ENDIF
RETURN this.Balance()
#IF COMPILE_TESTCASES
TEST_SUITE Withdrawl method
TEST_CASE (41488266) Positive
withdrawl
*-- setup
LOCAL lnBalance
lnBalance = oApp.nBalance
oApp.nBalance = 100.00 && reset to
stable state
*-- do test
LOCAL lnReturn
lnReturn = oApp.Withdrawl( 50.00 )
_FUnit.isEqual( 50.00, oApp.nBalance )
_FUnit.isTrue(
TYPE("lnReturn")=="N", "A non-numeric data type was
returned.")
_FUnit.isEqual( 50.00, oApp.nBalance )
*-- cleanup
oApp.nBalance = lnBalance
ENDTEST_CASE
#ENDIF
ENDFUNC
ENDDEFINE
|
Figure 1: Actual Code with FUnit Test Cases
Once all the Test Cases have been discovered and executed, the results are presented to the user. Figure 2 shows the total results from running the
code in Figure 1.
As mentioned before, the
main output is written to a Test Result log (Figure
3). In addition to the totals, as displayed in the dialog, the
result file contains which Test Suites and Test Cases were run, and the output
results from each. By default the result file is built in a subfolder of the
current folder with the name of Log\Result.log.
If the Coverage is enabled, the number of executable lines touched and missed is accumulated, plus
the percentage covered is calculated. In our example (Figure 3), the source code that was executed is
included. The vertical bars on the left margin are the lines that were
actually executed during the execution of the test cases.
Figure 2: Test Result dialog
For better readability in this document, I color
coded the example in Figure 3. The test
suite and test case names are displayed in black. The test result is green for
those that passed, and red for those that failed. The blue sections are the
coverage output. At the end, the total result of all the tests are in purple.
|
******************************
Suite:
Balance method
------------------------------
Test Case:
EXAMPLE.Accounting.BALANCE.Balance method.(42158287) Ensure the Balance
method returns the proper value.
: Passed [sec: 0.012]
Lines Touched: 1
Lines Missed: 0
Percentage Covered: 100.00%
==================================================
*-- return the current balance
| RETURN THIS.nBalance
==================================================
******************************
Suite:
Deposit method
------------------------------
Test Case: EXAMPLE.Accounting.DEPOSIT.Deposit
method.(42715240) Positive amount
: Passed [sec: 0.014]
------------------------------
Test Case:
EXAMPLE.Accounting.DEPOSIT.Deposit method.(43001537) Negitive amount
Failed: The amount was add as a negative to the balance.
Failed: The amount was not add to the balance, but the balance is
still off.
: Failed [sec: 0.011]
Lines Touched: 3
Lines Missed: 0
Percentage Covered: 100.00%
==================================================
| LPARAMETERS tnAmount AS NUMBER
*-- Add deposit to balance
| THIS.nBalance = THIS.nBalance + tnAmount
*-- return new balance
| RETURN this.Balance()
==================================================
******************************
Suite:
Validate properties
------------------------------
Test Case: EXAMPLE.Accounting.INIT.Validate
properties.(41952554) Validate nBalance property
: Passed [sec: 0.011]
Lines Touched: 2
Lines Missed: 0
Percentage Covered: 100.00%
==================================================
*-- set default balance
| nInitBal = 0.00
| this.nBalance = m.nInitBal
==================================================
******************************
Suite:
Withdrawl method
------------------------------
Test Case:
EXAMPLE.Accounting.WITHDRAWL.Withdrawl method.(41488266) Positive withdrawl
: Passed [sec: 0.017]
Lines Touched: 4
Lines Missed: 0
Percentage Covered: 100.00%
==================================================
PARAMETERS tnAmount as Number as Number
*-- if withdrawl amount is less then or equal to
balance
| IF tnAmount <= this.Balance()
*-- subtract amount
| this.nBalance = this.nBalance - tnAmount
| ENDIF
*-- return new balance
| RETURN this.Balance()
==================================================
Start Time: 11/17/2006 10:34:22
End Time: 11/17/2006 10:34:23
--------------------
Lines Touched: 9
Lines Missed: 0
Coverage: 100.00%
--------------------
TestCase ran: 5
Passed: 4
Failed: 1
Excluded: 0
Disabled: 0
Methods with TestCase: 4
Total Methods: 4
|
Figure 3: An example of the Result.log file
In the file FUnit.H are listed a set of directives
that are used to create and control the test cases we embed in our code. A
reference to the Funit.H must to include in code before using any of the directives.
It can included as a #INCLUDE statement, from the Class menu pad’s include file
option, and even referenced from a higher level included.
With the exception of the
COMPILE_TESTCASES, all the directives from this list evaluate at compile time
to comment lines, namely they are replaced with an asterisk. They must be the
first word on the line to be effective. Any statement, comment, or naming
methodology can be placed after the directive.
In the section below, the directives have been
divided into four separate sets to better understand each sets usage. Let us
examine each now.
|
Test Structure
|
Description / Usage
|
|
COMPILE_TESTCASES
|
If true, test case code can be checked by the compiler.
Set to false for runtime.
|
|
TEST_SUITE [<name>]
|
Defines a Test Suite grouping of Test Cases.
|
|
TEST_CASE [<name>]
|
Start of a test case code block
|
|
ENDTEST_CASE
|
End of test case code block
|
Table 1: FUnit Structure
directives
The first set of directives that we will discuss
is the Test Structure preprocessors. They are used to define the Test Block, Test Suite(s), and the Test Cases declared in our code blocks. As we compare
these directives to how they are used in the example of Figure 4 to see their basic usage.
|
#IF COMPILE_TESTCASES
TEST_SUITE Test Suite Name / Title
*------------------------------------------------------------
*
Description: Test Case Description
*------------------------------------------------------------
* Id
Date By Description
* 1
11/14/2006 GLR Initial Creation
*
*------------------------------------------------------------
TEST_CASE (33424823)
Test Case Name / Title
*--
setup
*--
do test
*--
cleanup
ENDTEST_CASE
#ENDIF
|
Figure 4: Test Suite definition using the FUnit directive.
The first one we encounter is the
COMPILE_TESTCASES directive. It is used as an expression in a #IF statement.
This has two usages: First is to provide a block marker signifying the
beginning of a Test Block. Because, by default, the value of the directive is
False, the block of code is never included in the compilation, therefore does
not make it to the actual run-time code. The second purpose is when it value
is set to True. This, of course, allows the compiler to compile the code
inside the Test Block. This provides a means to validate the syntax of the
Test Cases. There is a method at _FUnit.Maint.Misc.Compile() that is for the purpose of testing the syntax of the test cases. We talk more in
detail when look at the method.
The next directive we notice is the TEST_SUITE statement. This one defines the suite the following test cases are associated
with. The only real purpose of this directive is to aid in readability of the
output result log. Generally a name or comment follows the directive. This
part is what is included in the result file. My rule of thumb is to have it describe
the purpose of the test cases set that follow as a group.
The next two directives, TEST_CASE and ENDTEST_CASE, define the beginning and end of a test case code block. Each test case must begin with
TEST_CASE directive followed be some name or definition. By rule, I include a
unique identifier to help locate the test case if it fails. It also is good if
needing to reference the test case in a document or bug tracking system. In
our example, the number inside the parenthesis is auto-generated from the Intellisense shortcut using the SYS(3) function. Also remember that all test cases must end with an ENDTEST_CASE
directive.
In the test case block of Figure 4 are three lines of comments: setup, do
test, and cleanup. I added these to illustrate the need that all test cases
need to isolate themselves from all other code. Be sure to declare all
variables as Local variables, and if you open a table then close it before
ending the test. In other words, if you make a mess, clean up after yourself.
Test cases can not be nested, nor
can they use any references to the THIS objects. Even though they may be
executed inside the scope of the method they are actually executed with a
ExecScript() function, and therefore outside the objects scope. All references
to any apart of the application must be done explicitly through the application
object. See Table 2 for more rules
involving writing Test Cases.
|
DO
|
DONT
|
|
Explicitly reference object through from the root
application object and drill down to object in question.
|
Don’t use the THIS, THISFORM, or THISFORMSET object
reference.
|
|
|
|
|
|
|
Table 2: The DOs and DONTs in write Test Cases
Don’t forget the end the Test Block with a #ENDIF to compliment the #IF.
To aid in developing Test Cases faster, I have included a small set of Intellisense shortcuts. See Using Intellisense for quick test suites for more
information on these shortcuts.
This set of directives help control how FUnit
views any of the Test Cases.
|
Test Case Control Attributes
|
Description / Usage
|
|
TESTCASE_DISABLED [<comment>]
|
If present in test case, skip test case and log as
skipped.
|
|
TESTCASE_EXCLUDE [<comment>]
|
Exclude all test cases in the method.
|
|
TESTCASE_PROPERTIES_ONLY
|
The Test Case is only for testing properties. If Coverage is ON, do not output coverage results or source code
|
Table 3: Test Case Control Attributes
The directives defined in Table 3 help the Test Case developer when writing the test cases. Sometimes, we start constructing a Test Case only to
discover we need more information before we can complete it. There are two
directives to the rescue. The first one is the TESTCASE_DISABLED directive. If place inside a Test Case, the test case is not ran. It does appear in the result log as disabled,
to help us remember that we need to return to it and complete it. It is not
logged as a Passed or Failed test case, just skipped. You can add a comment
after the directive to have a clearer understanding as to why the test case has
been disabled.
The TESTCASE_EXCLUDE is like the TESTCASE_DISABLED, but instead affecting only the current test case, it affects all test cases in
the method. It is basically used when a method has become obsolete in the code
or there are no meaningful test cases for it. An event that only has a
DoDefault() statement really does not require a test case. Any comment that
follows the directive is for documentation of the test suites only.
The last directive in this set is the
TESTCASE_PROPERTIES_ONLY. Unlike the other two in this set, this one
indicates that the test cases in this method are primarily for testing the
properties of the object. It is more for documentation only. It is currently
not used or monitor by FUnit, but maybe in future releases.
The next two directives are for restricting test
case to version of Visual FoxPro and the Operating System (see Table 4). You can have more then one of the both
of these two. Define a directive for each version that the test case can
properly function. If omitted, all versions are considered legal. If the test
case is considered outside the version, it is omitted from the result log.
|
Environment Control Attributes
|
Description / Usage
|
|
TESTCASE_VFP_Version
|
Defines the minimum VFP version the Test Case will run under. I.E. TESTCASE_VFP_VERSION 8.0
|
|
TESTCASE_OS_Version
|
Defines the minimum OS version. I.E. TESTCASE_OS_VERSION
5.0 && WinXP
|
Table 4: Environment Control Attributes
The TESTCASE_VFP_Version directive uses the VERSION(5) as a point of comparison, and the TESTCASE_OS_Version directive uses expression VAL(SUBSTR(OS(1),9)) to get the major and minor version number.
The next set helps in classifying the test case
as different types of tests. By default, if none of these are in the test
case, the test case is in all type classifications, and executes in all test
runs.
I have provided seven best know (at
least to me) test types. But additional types can be added to the FUnit.H file to create more. You can place as many of these directives in a test case block
as need. We inform the FUnit object prior to the run which type(s) we which to
run, and the system restricts the test execution to only those marked with the
proper test type directives. We do the restriction by assign a string with a
comma delimited set of types to the _FUnit.Maint.Types property. You DO NOT need
to prefix the types with the TESTTYPE_ prefix; it is implied. The property is
case insensitive and if set to empty string, all tests are ran.
|
Test Case Types
|
Description / Usage
|
|
TESTTYPE_BVT
|
Build Verification Test or Smoke Test
|
|
TESTTYPE_REGRESSION
|
Regression Test
|
|
TESTTYPE_FUNCTIONAL
|
Functional Test.
|
|
TESTTYPE_STRESS
|
Stress Test
|
|
TESTTYPE_PREFORMANCE
|
Performance Test
|
|
TESTTYPE_UNIT
|
Unit Test
|
|
TESTTYPE_UAT
|
User Acceptance Test
|
Table 5: Test Case Types
Figure 5
is an example of the assigning the Types property to only execute tests marker
as either “BVT”,”UNIT”, or “UAT”.
|
DO FUnit
_FUnit.Maint.Types = “BVT,UNIT,UAT”
? _FUnit.Run( oApp )
Release _FUnit
|
Figure 5: Assign Classification Types
A Test Suite (and all Test Cases within it) is part of a Preprocessor block. The Preprocessors allow us to define the Test
Suites and Cases much the same fashion as we would with other Visual FoxPro statements.
To gain access to the predefined preprocessors we have to include the header
file FUnit.H. I have a series of header files with one main one that I
include in all my projects. It is here that I add the statement to include
FUnit header file.
Figure 6: FUnit.h include definition
Let’s take a look at a standard Test Suite block.
The first thing we encounter is the #IF statement.
The COMPILE_TESTCASES is defaulted to False (.F.). This preprocessor prevents
the test code from being included in the actual runtime compiled code that it
tests. We will discuss it more when we talk about the Compile method of the
class. But for now, it represents the beginning of a test block. A test block
can be place anywhere in a method, event, or subroutine. Multiple test blocks
can be defined in any chunk of code. But as a rule of thumb for myself, I
generally place all my test blocks at the end of the routine they are related
to.
|
****************************************
#IF COMPILE_TESTCASES
TEST_SUITE Test Suite Name / Title
*------------------------------------------------------------
*
Description: Test Case Description
*------------------------------------------------------------
* Id
Date By Description
* 1
11/14/2006 GLR Initial Creation
*
*------------------------------------------------------------
TEST_CASE (33424823)
Test Case Name / Title
*--
setup
*--
do test
*--
cleanup
ENDTEST_CASE
#ENDIF
|
Figure 7: Test Suite example
The next statement in the test
block is the TEST_SUITE declaration statement. These preprocessors aids in
classifying the test cases into smaller blocks. The title or name of the suite
should be descriptive of the purpose of the test cases defined in it. When the
test cases are executed, the Test Suite title is outputted to the log along
with the name of the class library, class name, and method or event name. All
these help in find the test case if it is reported as failed.
In order to speed up the writing of FUnit Test
Suites and Test Case, I have developed a few Intellisense keywords. The output of these keyword can be seen in Figure
8, 9, and 10.
Use the TEST_SUITE keyword to generate the
framework for a new Test Suite. It will have one Test Case framework already include. Complete the Suite and Test Case names or title to help identify
it in the result log. The large comment block is included to provide a means
to track the history of changes and offer a place for large description of the
Test Case. As in the Test_Case keyword, the TestCase block has a number inside
a pair of parenthesis. This is auto-generated Test Case number. It is from the SYS(3) function and may not be entirely unique, but come close enough for
now. I use this number in Code References at times to quickly locate the Test
Case that has failing.
|
Intellisense keyword
|
Description
|
|
Test_Suite
|
Generate a Test Suite block. (See Figure 8)
|
|
Test_Case
|
Inserts a Test Case into a Test Suite block. (See Figure 9)
|
|
NoTest
|
Generates a Test Suite and mark it as excluded. (See Figure 10) Generally these are placed as
markers. They will be logged in the result log as a reminder that the not
all the test cases have been defined. They will be neither flagged as a pass
or a fail, just skipped.
|
Table 6: Inellisense keyword
for FUnit
The TEST_CASE keyword is used to include
additional test case blocks to existing test suites.
|
****************************************
#IF COMPILE_TESTCASES
TEST_SUITE Test Suite Name / Title
*------------------------------------------------------------
*
Description: Test Case Description
*------------------------------------------------------------
* Id
Date By Description
* 1
11/14/2006 GLR Initial Creation
*
*------------------------------------------------------------
TEST_CASE (33424823)
Test Case Name / Title
*--
setup
*--
do test
*--
cleanup
ENDTEST_CASE
#ENDIF
|
Figure 8: Test Suite example from using TEST_SUITE
|
*------------------------------------------------------------
*
Description: Test Case Description
*------------------------------------------------------------
* Id
Date By Description
* 1
11/14/2006 GLR Initial Creation
*
*------------------------------------------------------------
TEST_CASE (33424823)
Test Case Name / Title
*--
setup
*--
do test
*--
cleanup
ENDTEST_CASE
|
Figure 9: Test Case example using TEST_CASE
|
****************************************
#IF COMPILE_TESTCASES
TEST_SUITE Test Suite Name / Title
TESTCASE_EXCLUDE (36738281) Comment
as to why excluded.
#ENDIF
|
Figure 10: Test Suite is marked as Excluded using NOTEST
The FUnit class is designed to Unit Test
application that stem from a single application object. Most frameworks are
built as object orientated structures, where all the objects spanned from a
root object. The case of the following example we will refer to it as oApp.
The code in Example.prg provides a sample of using FUnit.
The first step is to establish the FUnit object
itself. We do this by calling the FUnit.prg from the command window.
|
oApp = MyApp() && Start
Application to be tested.
DO FUnit.prg && Start
FUnit.
|
Figure 11: Launching FUnit
Up on return, a public variable is declared by
the name of _FUnit. This variable will be how we run our test cases. (See The FUnit objects) The Run method starts the
test run against the entire object model, in this case the oApp object. It
returns a pass (true) or fail (result) as a result. If any test case fails, a
false return value occurs. But if the all pass, a true value is returned. We
call the Run method, and pass it a reference to the oApp object. It scans the
objects PEM looking for methods that contain Test Cases. If a child object is discovered, it scans the child too. If the _FUnit.Maint.ParentOnly is set to
true, only the object passed is scan, and none of the child objects.
Figure 12: Run all test cases
in an application
But not always do we want to test the entire
application. Sometimes, we only need to test a smaller portion. We can do
this by passing a reference to object inside the main application. Figure 13 is an example of performing test cases
in a single object.
|
? _FUnit.Run( oApp.Forms[1].cmdSave )
|
Figure 13: Run test cases only in the cmdSave button.
By default, when the all the test cases have
been located and executed, the FUnit system displays the result dialog. From
this dialog, you can view the result of the tests.
Next, let us examine some of the advanced
features incorporated into the FUnit system. On the FUnit.Maint object we have
a property called Silent. This property suppresses the result dialog from
appearing. This is good to automation runs. There is also a Iteration
property that tell FUnit how many times to run each of the test cases. By
default they are only ran once to test run.
Off the FUnit.Maint object we have few support
child objects. One these is the Menu object. It displays a FUnit menu from
the System Menu. The Visible property controls whether the menu is displayed
or hidden. The Enabled property enabled and disabled the menu.
Another child support object is the Coverage object. It includes coverage analysis while the tests are being done. The results
are displayed as the number of code lines touch, the total number code lines,
and the percentage of lines touched. The idea is write the test cases to
achieve 100 percent coverage. If the IncludeCode property is set to true, then
the source code is added to the test result log. Each line touch (or executed)
will have a bar ( | ) in the left margin.
This section describes the various objects and
their properties and methods that can be used to properly unit test the
routines and applications.
In Figure
11 we have the FUnit Object Modal hierarchy. Even though not
defined here, the main class (_FUnit) is derived from the fuAssert class that
is turn derived from the fuSupport class. I separated them for maintenance
purposed. The Maint class is separate from the main class to ease the finding
Test Assert methods from the internal and maintenance methods found in the
Maint class. Again, for ease of maintenance and future expansions, I have
isolated the other general functionality into separate classes. These will be
explained in more detail as we study each.
|
_FUnit
|
|
|
|
|
Maint
|
|
|
|
|
|
|
|
|
Counter
|
|
|
|
|
|
|
Coverage
|
|
|
|
|
|
|
Exception
|
|
|
|
|
|
|
Log
|
|
|
|
|
|
|
Menu
|
|
|
|
|
|
|
Misc
|
|
|
|
Figure 14: FUnit DOM
Before we dig deeper into the classes
themselves, let me help you understand how the support classes get added to the
Maint object. Actually the Maint class does not explicitly reference all of
the support object. It scans the FUnit folder looking for filenames that match
the pattern of “FUnit_*.prg”. It then scans the file and retrieves the class
name, and load the class as a support