
FUnit
Embedded Unit Testing
for Visual FoxPro
Version 1.20
Product from the
LifeCycle series
Developed by GLR software © 2006-2011
Authored by Gregory Reichert
Information in this document, including URL and other Internet Web site references, is subject to change without notice. Unless otherwise noted, the example companies, organizations, products, domain names, e-mail addresses, logos, people, places and events depicted herein are fictitious, and no association with any real company, organization, product, domain name, e-mail address, logo, person, place or event is intended or should be inferred. Complying with all applicable copyright laws is the responsibility of the user. Without limiting the rights under copyright, no part of this document may be reproduced, stored in or introduced into a retrieval system, or transmitted in any form or by any means (electronic, mechanical, photocopying, recording, or otherwise), or for any purpose, without the express written permission of GLR software.
The names of actual companies and products mentioned herein may be the trademarks of their respective owners.
© 2011. GLR software. All rights reserved.
6822 Kirby Arms Drive
Memphis, Tennessee, 38115,
Phone: 901-730-1166
Email: GregReichert@GLRoftware.com
Web Site: http://www.GLRsoftware.com
Table of Contents
Using
Preprocessors to define Test Cases
Writing
your first FUnit Test Case
Using
Intellisense for quick test suites
Tables
Table
1: FUnit Structure directives
Table
2: The DOs and DONTs in write Test Cases
Table
3: Test Case Control Attributes
Table
4: Environment Control Attributes
Table
6: Inellisense keyword for FUnit
Figures
Figure
1: Actual Code with FUnit Test Cases
Figure
3: An example of the Result.log file
Figure
4: Test Suite definition using the FUnit directive.
Figure
5: Assign Classification Types
Figure
6: FUnit.h include definition
Figure
8: Test Suite example from using TEST_SUITE
Figure
9: Test Case example using TEST_CASE
Figure
10: Test Suite is marked as Excluded using NOTEST
Figure
12: Run all test cases in an application
Figure
13: Run test cases only in the cmdSave button.
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 the Unit Test code is embedded in 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 a part 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 a part of the Debug version of production projects. After coding some or all of the test cases, we simply call the FUnits’ 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.
What’s new in version 1.20?
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 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 Using Intellisense for quick test suites 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 cannot 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, it does 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 than 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.
|
#INCLUDE Include\FUnit.H |
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, Figure 9, and Figure 10Error! Reference source not found..
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.
|
? _FUnit.Run( oApp ) |
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 14 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 a part of the final production version of the application.
The FUnit utility accepts an optional string parameter of options. The following are currently available. Any combinations of these options are allowed.
|
Name |
Description |
|
Debug |
Places the FUnit |
|
Parallel |
Run the test case in a parallel processing mode. |
|
DO FUnit WITH “Parallel Debug” |
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
N/A
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
N/A
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.
In conclusion, I would like to thank those who have help is perfecting this and other tools and utilities I have developed.