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 object. This approach was introduced
when I started refactoring the code, and decided to move the Coverage and Menu classes to their own program file. This allows for expandability of FUnit
without having to modify the main routine. All add-ons should be based on the
fuBase class found in FUnit.prg. I have plans to offer an Assert (those
generated from the FoxPro Assert statement), and possible Undeclared Local
Variables (generated from the LanguageOptions property) add-on modules in
upcoming release.
The _FUnit object is the main object. It
contains methods that are used to perform and evaluate the results of tests. A
public variable is created when the FUnit.prg is called.
|
DO FUnit
? _Funit.Version
RELEASE _Funit
|
Also, you can gain reference directly by
instantiating the FUnit object directly with the uses of the NEWOBJECT or
CREATEOBJECT functions.
|
myFUnit = NewObject(“FUnit”,”FUnit.prg”)
|
It is not considered wise to include the FUnit
routines in our application, for it will then be apart of the final production
version of the application.
Objects
Exposed objects of the FUnit object.
|
Name
|
Description
|
|
Maint
|
Internal object of support routines.
|
Properties
Exposed properties of the FUnit object.
|
Name
|
Description
|
|
Version
|
Current version of the object.
|
Methods
/ Events
The Methods and Events exposed for the object
including their syntax.
Run( toObject as Object ) : boolean
Start the Unit Test run. Pass the method a reference of the
object to test (mainly the application level object), the object and all the
children will be scan and their Test Cases will be executed.
Log(
tcMsg as variant ) : logical
Output the result of the argument passed to the Result log.
No explicit carriage return included.
Sleep( tnMilli as number ) : void
Makes the system sleep for N number of milliseconds.
Keyboard( tcKeyStrokes as String [,tlSystem
as Boolean] ) : void
Send a string of keystrokes to the VFP keyboard buffer. If
the tlSystem argument is true, the keystrokes are sent to the Windows keyboard
buffer. See “ON KEY LABEL” and SendKeys of the WScript.Shell for special words
to represent special keystrokes. Special words must be enclosed in
curly-brackets.
Mouse( tnRow as number,tnCol as number
[,tnEffect as bitset) : void
Move and clicks the mouse buttons. The tnRow and tnCol
arguments are the coordinents of the mouse. The tnEffect argument is a bit set
with the following values:
|
Bit
|
Definition
|
|
1
|
Click double-click, otherwise single click.
|
|
2
|
Click right button, otherwise left button.
Add the bits together to create the desired result
|
.
CloseModalDialog()
Close the foremost window with a Control+Enter keystroke.
Assert Methods
isEqual(
tvVal1 as variant, tvVal2 as variant [,tcMsg as string] ) : Boolean
isNotEqual(
tvVal1 as variant, tvVal2 as variant [,tcMsg as string] ) : Boolean
Compares two values to see if they are exactly or not
exactly equal. If fails, the message is writing to log. If the tcMsg argument
is provided, it is writing instead of the canned message.
isSame(
tvVal1 as variant, tvVal2 as variant [,tcMsg as string] ) : Boolean
isNotSame(
tvVal1 as variant, tvVal2 as variant [,tcMsg as string] ) : Boolean
Compares two values they are equal or not equal. Good for
comparing object references. If all properties are the same the result is
true. If fails, the message is writing to log. If the tcMsg argument is
provided, it is writing instead of the canned message.
Contains(
tvVal1 as variant, tvVal2 as variant [,tcMsg as string] ) : Boolean
NotContains(
tvVal1 as variant, tvVal2 as variant [,tcMsg as string] ) : Boolean
Determines if the first value is or is not contained in the
second value.
isGreater(
tvVal1 as variant, tvVal2 as variant [,tcMsg as string] ) : Boolean
isGreaterOrEqual(
tvVal1 as variant, tvVal2 as variant [,tcMsg as string] ) : Boolean
Evaluates whether the first value is greater then or greater
then or equal to the second value.
Isless(
tvVal1 as variant, tvVal2 as variant [,tcMsg as string] ) : Boolean
isLessOrEqual(
tvVal1 as variant, tvVal2 as variant [,tcMsg as string] ) : Boolean
Evaluates whether the first value less then or less then or
equal to the second value.
isTrue(
tlVal1 as boolean [,tcMsg as string] ) : Boolean
isFalse(
tlVal1 as boolean [,tcMsg as string] ) : Boolean
A straight logical evaluation asserts.
isNull(
tvVal1 as variant [,tcMsg as string] ) : Boolean
isNotNull(
tvVal1 as variant [,tcMsg as string] ) : Boolean
Determines whether the value is either null or not.
isEmpty(
tvVal1 as variant [,tcMsg as string] ) : Boolean
isNotEmpty(
tvVal1 as variant [,tcMsg as string] ) : Boolean
Evaluates whether the values is empty or not.
Passed(tcMsg
as string ) : Boolean
Failed(tcMsg
as string ) : Boolean
These are used when a home-grown condition has evaluated the
test case has either passed or failed.
isObject(
tvVal1 as variant [,tcMsg as string] ) : Boolean
isNotObject(
tvVal1 as variant [,tcMsg as string] ) : Boolean
Determines of the value is a object or not.
IsArrayEqual(
ref taVal1 as variant, ref taVal2 as variant [,tcMsg as string] ) : Boolean
isNotArrayEqual(
ref taVal1 as variant, ref taVal2 as variant [,tcMsg as string] ) : Boolean
Compares two arrays to determine if they have the same
values or not.
isCollectionEqual(
tvVal1 as collection, tvVal2 as collection [,tcMsg as string] ) : Boolean
isCollectionNotEqual(
tvVal1 as collection, tvVal2 as collection [,tcMsg as string] ) : Boolean
Compares to collections to determine if they have the same
values.
The Maint contains engine of the Funit class.
Also, it contains support objects that separate additional functionality. All
but the Coverage and Menu class are located in the main FUnit.prg file.
Objects
|
Name
|
Description
|
|
_FUnit.Maint.Counter object
|
Contains the entire pass, fail, exclude, etc counters.
|
|
_FUnit.Maint.Coverage object
|
Generate Coverage information for the result log. Found
in the FUnit_Coverage.prg.
|
|
_FUnit.Maint.Exception object
|
Contains a list of class lib, class, method, etc. that
never apart of the test runs.
|
|
_FUnit.Maint.Log object
|
Manages the output result log file.
|
|
_FUnit.Maint.Menu object
|
Adds a menu to system menu for the FUnit features. Found
in the FUnit_Menu.prg.
|
|
_FUnit.Maint.Misc object
|
Miscellaneous methods.
|
Properties
ApplName
Full path to main application. (Default: “”)
ApplObjectName
Main Public object to the application. (Default: .NULL.)
ClassLibName
Current class library name being processed. (Default: “”)
ClassName
Current class name being processed. (Default: “”)
ClassObj
Object reference to current class. (Default: .NULL.)
CompilePath : string
Compilation Path. Compile method uses this to locate Test
Case routines. (Default: Classes, Prg, Menus, Reports, Data)
DebugMode : boolean
Used when debugging this class. (Default: .F.)
EndTime
The time the test run ended. (Default: “”)
Err
Last error object thrown. (Default: .NULL.)
Errmsg
Last error message. (Default: “”)
Interation : number
The number of times any one of the test cases is ran.
(Default: 1)
llPass : boolean
Last test case passed. (Default: .F.)
llRan : boolean
Last test case ran. (Default: .F.)
llSkipped : boolean
Was the last test case marked as skip?. (Default: .F.)
MethodCode
Enter code of the current method. (Default: “”)
MethodName
Current method being processed. (Default: “”)
ParentOnly ; boolean
If true, scan first level of the heritage only. If False,
then scan first and all child levels too. (Default: .F.)
Result : number
Last test case result. If =0 then it passed. (Default: 0)
Seconds : boolean
Specifies if seconds are displayed. (Default: .F.)
Silent : boolean
Prevents the MessageBox from appear at the end of the run.
(Default: .F.)
StartTime
The time the test run started. (Default: “”)
TestCode
The current test case code being executed. (Default: “”)
TestCaseName
Name of the current test case. (Default: “”)
TestSuiteName
Name or title of the current test suite. (Default: "{Unknown}")
Types : string
test run category (BVT, Regression, UAT). (Default: “”)
Version : number
Product Version Number. (Default: FUNIT_VERSION)
Methods
/ Events
Technically, all methods in this
and child class at this time are internal use only. Review each of the child
class for properties and methods that a safe to use.
The Counter class tracks all the counters.
Objects
N/A
Properties
nDisabled : number
Total number of Disabled Test Cases.
nExcluded : number
Total number of Methods excluded.
nFailed : number
Total number of Test Cases that failed.
nMethodTestCase : number
Total number of method that contained one or
more Test Cases.
nPassed : number
Total number of Test Cases that passed.
nTestCase : number
Total number of Test Case in all.
nTotalMethods : number
Total number of method searched while scanning
for test cases.
Methods
/ Events
The Coverage class generates the coverage
analysis using the SET COVVERAGE feature of VFP.
Objects
N/A
Properties
Enabled : boolean
When true, Coverage Analyses is preformed. The
number of lines touched and total lines in the method are added to Result log.
Filename : string
The name and path to the final Coverage Output log file. Same you would get if the ran the application with the SET
COVERAGE TO set.
IncludeCode : boolean
If true, a marked up version of the method code
is included in the result log file.
nTouched : number
The total number of lines touched throughout the
entire test case run.
nTotal : number
The total number of lines in all methods encountered
during the test case run.
Methods
/ Events
The Exception class determines if a class or
method is ignored in the source code from processing the test cases. The file
called “FUnit.Exception” is a carriage-return delimited list of classes and
method names to be ignored.
Objects
N/A
Properties
N/A
Methods
/ Events
N/A
The Log class generates the Test Result log.
Objects
N/A
Properties
Filename : string
The name and path to the Result log. Defaults
to “.\log\Result.log”.
Methods
/ Events
Show() : void
Display the
Result log in a window.
The Menu class adds the FUnit menu pad to the
System Menu.
Objects
N/A
Properties
Enabled : Boolean
When true, the FUnit menu pad is enabled,
otherwise disabled.
Visible : Boolean
When True, the FUnit menu pad appears, but when
False, it is removed.
Methods
/ Events
Add2Menu( tcPrompt as
string,
tcTitle as string,
tcCmd
as string,
tcMarker as string ) : void
Add a menu item
to the FUnit popup menu under the FUnit menu pad.
Arguments
tcPrompt: The
caption displayed to the user.
tcTitle: The
tooltip that is display when the mouse hovers over the item.
tcCmd: The
command that executed when the item is clicked.
tcMarker: A
string that holds the object path to a logical property.
The Misc class contains various other methods
that did not fit into the other class.
Objects
N/A
Properties
N/A
Methods
/ Events
Compile( [tlShow as Boolean] ) : boolean
Compiles all the program files
(PRG) and the Class Libraries (VCX) discovered in the path defined in the
_FUnit.Maint.CompilePath. Use this routine to test the syntax of the Test
Code. Passing the Compile method a logical True value will instruct the method
to display on the screen the folder currently scanning for files to compile.
This method temporarily alters the
FUnit.H file by changing the COMPILE_TESTCASES directive from False (.F.) to True (.T.). This allows the test case code to be compiled with the natural code.
The following are the minimal requirement for
using FUnit tool.
- Microsoft Visual FoxPro 8.0 or greater.
In conclusion, I would like to thank those who
have help is perfecting this and other tools and utilities I have developed.